---
title: "AWS LightSail: The Final Piece of the Replit to Production Puzzle"
description: "Learn how to automate deployments using Git tags, GitHub Actions, and AWS Lightsail Container Service. This guide covers setting up date-based tags as deploy triggers, creating production-ready Dockerfiles, configuring automated CI/CD pipelines, implementing health checks for zero-downtime deployments, and establishing rollback strategies. Includes practical examples for Python Flask and Node.js apps with container best practices."
image: "https://cdn.sethserver.com/blog-images/aws-lightsail-the-final-piece-of-the-replit-to-production-puzzle-social.webp"
---

# AWS LightSail: The Final Piece of the Replit to Production Puzzle

I worked at a quite a few places with manual deployments.

The kind of deployments that require someone to SSH into production, run a series of commands in the correct order, maybe run a database migration or two, and pray nothing breaks.

Something inevitably breaks. The "deployment person" goes on vacation or gets hit by the proverbial bus (e.g. gets laid off so execs can buy new Ferraris to celebrate their cost-cutting abilities). The backup... person? misses a step.

I've learned two things:

1. Always test like a user (I once took down an e-commerce site on launch weekend by not testing in IE because Firefox was shiny and new).
2. Deployments should be boring, automated, and reversible.

That's the whole game here. [Replit is a great place to build](https://www.sethserver.com/programming/insanely-rapid-ai-assisted-development-with-replit.html). But production needs logging, backups, health checks, and a deployment system that works even if the one hero engineer is out sick.

I use **tags** and **containers**. Git tags trigger GitHub Actions, which builds a Docker image and deploys it to **AWS Lightsail Container Service**.

This closes the loop:

- Replit is where you code.
- [GitHub is where you version and trigger automation](https://www.sethserver.com/programming/your-code-needs-to-live-somewhere-other-than-your-laptop.html).
- Lightsail is where the container runs, with health checks and a stable URL.
- Tags are the big red button that starts the whole thing.

---

## Tags: the "no bikeshedding" version number

Semantic versions are great for libraries. For a startup shipping constantly, they turn into "major or minor change" meetings. Gah.

Date-based tags are simpler: `YYYYMMDDHHMM`

```bash
tag=$(date +%Y%m%d%H%M)
git tag $tag
git push origin $tag
```

**Example:** `202501201430` = January 20, 2025 at 2:30 PM

That's it. A deploy is a **single action** with a clean audit trail.

### Why date tags work in real life

- **They sort correctly.** Newer tags are bigger numbers.
- **They're self-explaining.** "We deployed the 1430 build" is clear.
- **They avoid arguments.** Nobody debates minor vs patch.
- **They're searchable.** You can line up an incident timeline with deploy times.

A practical detail: tags are immutable by convention. You *can* delete and recreate them. Don't. Lock down who can push tags.

### Staging vs prod tags

If you want staging vs prod, suffix it:

```bash
git tag 202501201430-staging
git tag 202501201445-prod
```

Your GitHub Actions trigger needs to match both patterns (we'll do that below).

### Making it one command (so people actually do it)

If deploying takes three commands and a prayer, it won't happen consistently. Add an alias:

```bash
alias deploy='tag=$(date +%Y%m%d%H%M) && git tag $tag && git push origin $tag'
```

Then deploying is:

```bash
deploy
```

If you want staging:

```bash
alias deploy-staging='tag=$(date +%Y%m%d%H%M)-staging && git tag $tag && git push origin $tag'
alias deploy-prod='tag=$(date +%Y%m%d%H%M)-prod && git tag $tag && git push origin $tag'
```

---

## Why containers

Containers solve a bunch of annoying problems:

- Your app runs with the same OS + runtime + dependencies everywhere.
- The server stops being a weird pet you have to hand-feed.
- Rollback becomes "deploy the old image."

Lightsail Container Service is basically Docker hosting with fewer moving parts. No SSH. No nginx ritual. No server drift.

---

## Dockerfile (minimum viable container)

A basic Flask `Dockerfile`:

```dockerfile
FROM python:3.14-slim
WORKDIR /app

COPY requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt

COPY . .
EXPOSE 8080
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8080", "app:app"]
```

If your app listens on `8080`, Lightsail can run it.

### Use `.dockerignore`

Create `.dockerignore`:

```
.git
.env
__pycache__
*.pyc
.venv
node_modules
tests
README.md
.DS_Store
```

### Dependency caching (fast builds)

Keep this order:

```dockerfile
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
```

### Node apps (same idea)

If you're on Express:

```dockerfile
FROM node:24-slim
WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .
EXPOSE 8080
CMD ["node", "server.js"]
```

---

## Setting up GitHub Actions for container deployment

Push a tag. GitHub Actions builds. Lightsail deploys.

Create `.github/workflows/deploy.yml`:

```yaml
name: Deploy to Lightsail (on tag)

on:
  push:
    tags:
      # 12-digit timestamp tags
      - '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
      # Optional env suffixes
      - '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-staging'
      - '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-prod'

jobs:
  deploy:
    runs-on: ubuntu-latest

    env:
      AWS_REGION: ${{ secrets.AWS_REGION }}
      LIGHTSAIL_SERVICE_NAME: ${{ secrets.LIGHTSAIL_SERVICE_NAME }}
      LIGHTSAIL_CONTAINER_NAME: ${{ secrets.LIGHTSAIL_CONTAINER_NAME }}
      LIGHTSAIL_PUBLIC_PORT: ${{ secrets.LIGHTSAIL_PUBLIC_PORT }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Install AWS CLI + jq
        run: |
          sudo apt-get update
          sudo apt-get install -y jq

      - name: Set version from tag
        run: |
          echo "APP_VERSION=${GITHUB_REF_NAME}" >> $GITHUB_ENV

      - name: Get Lightsail registry auth + endpoint
        id: ls_registry
        run: |
          aws lightsail get-container-services-registry-login \
            --region "$AWS_REGION" \
            --output json > registry.json

          echo "REGISTRY_USERNAME=$(jq -r '.registryLogin.username' registry.json)" >> $GITHUB_ENV
          echo "REGISTRY_PASSWORD=$(jq -r '.registryLogin.password' registry.json)" >> $GITHUB_ENV
          echo "REGISTRY_ENDPOINT=$(jq -r '.registryLogin.registry' registry.json)" >> $GITHUB_ENV

      - name: Docker login to Lightsail registry
        run: |
          echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_ENDPOINT" -u "$REGISTRY_USERNAME" --password-stdin

      - name: Build image
        run: |
          docker build -t "$LIGHTSAIL_CONTAINER_NAME:$APP_VERSION" .

      - name: Tag + push image
        run: |
          IMAGE_URI="$REGISTRY_ENDPOINT/$LIGHTSAIL_CONTAINER_NAME:$APP_VERSION"
          docker tag "$LIGHTSAIL_CONTAINER_NAME:$APP_VERSION" "$IMAGE_URI"
          docker push "$IMAGE_URI"
          echo "IMAGE_URI=$IMAGE_URI" >> $GITHUB_ENV

      - name: Create Lightsail deployment JSON
        run: |
          cat > containers.json <<EOF
          {
            "containers": {
              "${LIGHTSAIL_CONTAINER_NAME}": {
                "image": "${IMAGE_URI}",
                "environment": {
                  "APP_VERSION": "${APP_VERSION}"
                },
                "ports": {
                  "${LIGHTSAIL_PUBLIC_PORT}": "HTTP"
                }
              }
            },
            "publicEndpoint": {
              "containerName": "${LIGHTSAIL_CONTAINER_NAME}",
              "containerPort": ${LIGHTSAIL_PUBLIC_PORT},
              "healthCheck": {
                "healthyThreshold": 2,
                "unhealthyThreshold": 2,
                "timeoutSeconds": 5,
                "intervalSeconds": 10,
                "path": "/health",
                "successCodes": "200-399"
              }
            }
          }
          EOF

      - name: Deploy to Lightsail
        run: |
          aws lightsail create-container-service-deployment \
            --region "$AWS_REGION" \
            --service-name "$LIGHTSAIL_SERVICE_NAME" \
            --containers file://containers.json \
            --public-endpoint file://containers.json
```

You'll notice this workflow does the same steps every time, in the same order. That's the point.

---

## Adding Secrets to GitHub

In your GitHub repo:

**Settings - Secrets and variables - Actions - New repository secret**

Add these:

- `AWS_ACCESS_KEY_ID`
- `AWS_SECRET_ACCESS_KEY`
- `AWS_REGION` (example: `us-east-1`)
- `LIGHTSAIL_SERVICE_NAME` (example: `my-app-service`)
- `LIGHTSAIL_CONTAINER_NAME` (example: `web`)
- `LIGHTSAIL_PUBLIC_PORT` (example: `8080`)

---

## Deploying to AWS Lightsail Container Service

### Step 1: Create a Container Service

In AWS Lightsail:

1. Go to **Containers**
2. **Create container service**
3. Pick a size (start small; you can scale later)
4. Give it a name that matches `LIGHTSAIL_SERVICE_NAME`

### Step 2: Add a health check endpoint

Add a `/health` endpoint in your app. Keep it dumb:

```python
@app.route("/health")
def health():
    return {"status": "ok"}, 200
```

Without this, Lightsail won't know if your app is alive or dead. It'll assume dead and keep restarting containers. Over, and over, and over, like a machine!

### Step 3: First deployment

Your first deploy can be done by pushing a tag:

```bash
tag=$(date +%Y%m%d%H%M)
git tag $tag
git push origin $tag
```

Then watch the GitHub Actions run. After it finishes, Lightsail should show a new deployment and a public URL.

---

## Viewing Container Logs

Two ways:

### Lightsail console

Lightsail - your container service - **Deployments / Logs** (AWS moves this around occasionally, but it's always in the service view).

### CLI

```bash
aws lightsail get-container-log \
  --service-name YOUR_SERVICE \
  --container-name YOUR_CONTAINER \
  --region YOUR_REGION
```

Log to stdout/stderr from your app. Don't write log files inside the container.

A small habit that pays off: log the version on startup.

```python
import os, logging
logging.basicConfig(level=logging.INFO)
logging.info("Starting app version=%s", os.getenv("APP_VERSION", "unknown"))
```

---

## Zero-downtime deployments

Lightsail does rolling deployments using the health check gate.

Your job:

- Make sure the container starts quickly.
- Make sure `/health` returns 200 once the app can serve traffic.
- Don't put slow database checks in `/health`.

If you need deeper checks, add a separate endpoint like `/ready` for your own monitoring.

---

## Rollback Strategy

Something broke. Time to rollback.

### Option 1: Deploy the previous tag (best)

Find the previous tag and redeploy it:

```bash
git tag -l --sort=-version:refname | head -10
git push origin 202501201400
```

This assumes your pipeline deploys on tag push and the older tag still exists.

### Option 2: Manual rollback in Lightsail (fastest in a panic)

In Lightsail, pick the last good deployment and redeploy it from the console. This is the "I need the site back up now" button.

### Option 3: Nuclear option (stop the bleeding)

Scale the service down or disable the public endpoint while you fix it. It's not elegant, but it's better than serving broken database writes.

---

## Database Migrations with Containers

Your container needs to run migrations. Two ways to do this:

### Option A: Run migrations in GitHub Actions

Add a step before the Lightsail deploy that runs your migration command against the prod DB.

Example (pseudo):

```yaml
- name: Run migrations
  run: |
    python -m alembic upgrade head
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL_PROD }}
```

This is my default because a failed migration stops the deploy.

### Option B: Run migrations at container startup

You can run migrations in your container entrypoint before starting the web server. This keeps CI simpler, but you need to ensure only one instance runs migrations. This isn't the best option, but lots of people do it, so it is "one way."

---

## Optimizing Docker Images

A few easy wins:

- Use `python:*-slim` or `node:*-slim` unless you have a reason not to.
- Keep `.dockerignore` tight.
- Don't install dev dependencies in production images.
- If builds are slow, look at what changes bust the cache (usually copying the whole repo too early).

If your image is huge, your deploy will feel huge.

---

## Troubleshooting Common Issues

**Health check fails**
- App isn't listening on `0.0.0.0`
- Wrong port (Lightsail expects what you declare)
- `/health` route missing or returning non-200

**Deploy succeeds but app errors at runtime**
- Missing env vars (set them in the deployment JSON / Lightsail config)
- Missing system packages (add them in the Dockerfile)
- Wrong start command (Gunicorn module path, Node entry file, etc.)

**Actions can't push to registry**
- AWS credentials don't have Lightsail permissions
- Wrong region (Lightsail resources are region-scoped)
- Service name / container name mismatch

---

## Real Deployment Example

Here's what an actual deploy looks like:

```bash
## In Replit (or locally)
git status
git add -A
git commit -m "Fix checkout flow and add /health"
git push

## Ship it
tag=$(date +%Y%m%d%H%M)
git tag $tag
git push origin $tag
```

Example output tag:

- `202501201430` (Jan 20, 2025 at 2:30 PM)

Then:

- GitHub Actions builds `web:202501201430`
- Pushes it to the Lightsail registry
- Creates a Lightsail deployment
- Lightsail waits for `/health` to pass
- Traffic shifts

---

## Action Items

- Add `/health` to your app.
- Add a Dockerfile that runs your app on `0.0.0.0:8080`.
- Add `.dockerignore`.
- Create a Lightsail Container Service.
- Add GitHub Actions secrets.
- Push a timestamp tag and watch one full deploy end-to-end.
- Practice one rollback while you're calm.

---

## Red Flags

- "Just SSH in and change it real quick."
- Tags that get deleted and recreated.
- [Deploys that require one specific person to be online](https://www.sethserver.com/programming/the-five-deadly-sins-of-deployments.html).
- Health checks that hit the database (great way to turn a small outage into a redeploy loop).
- Containers that write logs to files and then you can't find them.

---

## Key Takeaways

- A deploy should be a single, repeatable action.
- Tags are a clean deploy trigger and a clean audit trail.
- Containers make "what's running?" a factual question.
- Health checks are what turn "deploy" into "deploy safely."
- [Rollback should be the same pipeline as deploy](https://www.sethserver.com/startups/its-not-about-crushing-success-its-about-recovering-from-catastrophic-failure.html).

---

## Resources

- AWS Lightsail Container Service docs: https://docs.aws.amazon.com/lightsail/
- GitHub Actions docs: https://docs.github.com/actions
- Dockerfile reference: https://docs.docker.com/engine/reference/builder/
- Gunicorn docs: https://docs.gunicorn.org/

-Sethers


```json
{"@context": "https://schema.org", "@type": "BlogPosting", "headline": "AWS LightSail: The Final Piece of the Replit to Production Puzzle", "datePublished": "2026-03-03T15:09:40", "dateModified": "2026-09-17T19:48:16", "image": {"@type": "ImageObject", "url": "https://cdn.sethserver.com/blog-images/aws-lightsail-the-final-piece-of-the-replit-to-production-puzzle-social.webp", "width": 400, "height": 400}, "url": "https://www.sethserver.com/programming/aws-lightsail-the-final-piece-of-the-replit-to-production-puzzle.html", "author": {"@type": "Person", "name": "Seth Black", "url": "https://www.linkedin.com/in/seth-black-tx"}, "publisher": {"@type": "Organization", "name": "Sethserver", "logo": {"@type": "ImageObject", "url": "https://www.sethserver.com/static/images/PaperAirplane512.png"}}, "description": "Learn how to automate deployments using Git tags, GitHub Actions, and AWS Lightsail Container Service. This guide covers setting up date-based tags as deploy triggers, creating production-ready Dockerfiles, configuring automated CI/CD pipelines, implementing health checks for zero-downtime deployments, and establishing rollback strategies. Includes practical examples for Python Flask and Node.js apps with container best practices.", "articleSection": "Technology"}
{"@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [{"@type": "ListItem", "position": 1, "name": "sethserver", "item": "https://www.sethserver.com/"}, {"@type": "ListItem", "position": 2, "name": "Programming", "item": "https://www.sethserver.com/programming/"}, {"@type": "ListItem", "position": 3, "name": "AWS LightSail: The Final Piece of the Replit to Production Puzzle", "item": "https://www.sethserver.com/programming/aws-lightsail-the-final-piece-of-the-replit-to-production-puzzle.html"}]}
```
