Automating Deployment with Github Actions
23 Sept 2026, 2:33 am
Deploying to a Server You Can't Reach: Building a CI/CD Pipeline with AWS SSM and OIDC
From pushing code manually to building, testing, containerizing, and deploying every change automatically.
In the previous article, we learned how to spin up a functioning network and compute resources on AWS, and deploy our application on them. Let's remember a key architectural decision: our EC2 instance is in a private subnet, shielded from the internet.
Now we have a problem. It gets tedious to log in, pull code, and build our Docker images every time we make changes to our codebase.
Enter CI/CD.
CI/CD stands for Continuous Integration and Continuous Delivery/Deployment. It aims to streamline and accelerate the software development lifecycle.
Continuous Integration (CI) refers to the practice of automatically integrating code changes into a shared source code repository. Continuous Delivery and/or Deployment (CD) is a two-part process that refers to the integration, testing, and delivery of code changes. Continuous delivery stops short of automatic production deployment, while continuous deployment automatically releases the updates into the production environment. This means our deployment should happen every time we push code to our GitHub repo.
(If you need a refresher on our setup, check it out here: Terraform Deployment)
Here is the catch: our server has no public IP address. It sits in a private subnet behind a load balancer. GitHub Actions cannot SSH into it. There is no port 22 open to the internet. There is no bastion host.
So how do you deploy to a server you can't reach from the internet? Why This is More Complex Than a Typical CI/CD Setup
Typically, we would set up a pipeline that builds a Docker image, pushes it to a registry, then SSHes into a server with a stored SSH key to pull the image and restart the container.
None of that works for this setup.
The First Problem: The server is unreachable. The EC2 instance lives in a private subnet (10.0.10.0/24). Traffic from the internet goes through the Application Load Balancer, not directly to the server. There is no public IP. GitHub Actions cannot SSH in.
The Second Problem: Four containers, not one. This is not a single Docker image deployment. It is a docker-compose.yml stack with four interdependent services:
If the backend starts before PostgreSQL is healthy, the Alembic database migrations crash. If Nginx starts before the backend is healthy, it throws a 502 Bad Gateway. Docker Compose manages this dependency chain with health checks and depends_on conditions. So the deployment tool needs to orchestrate Docker Compose on the server, not just swap one container.
Problem 3: No stored credentials. I did not want AWS access keys sitting in GitHub Secrets. Keys do not expire. If they leak, they are valid until someone notices and manually revokes them. That could be weeks. Or months. In a personal project, probably never.
I needed a solution that was: • Keyless (no stored AWS crede…
https://dev.to/israeltheory/automating-deployment-with-github-actions-24i1