GEMVC Documentation
Complete guide to building modern, secure PHP applications with GEMVC framework
🚀 Comprehensive Guide: Deploying a Dockerized Application on a VPS with Traefik and Automatic SSL
This guide provides a complete, step-by-step walkthrough for setting up a secure, modern, and maintainable deployment environment for any containerized application.
Core Concepts
- VPS (Virtual Private Server): Your own private server space on the internet.
- Docker: A platform to package your application and its dependencies into a "container," which can run anywhere.
- Docker Compose: A tool for defining and running multi-container Docker applications. We use it to manage our services.
- Traefik: A modern reverse proxy. It automatically discovers our running application containers and routes traffic to them.
- Let's Encrypt: A free service that provides SSL certificates. Traefik will automatically get and renew these for us, enabling `https://` for our domains.
Part 1: Initial Server & Domain Setup
Prerequisites
- A VPS running a modern Linux distribution (like Ubuntu 24.04).
- A registered domain name (e.g., `your-cool-app.com`).
- The DNS "A" record for your domain (and any subdomains) pointed to your VPS's public IP address.
Step 1: Install Docker and Docker Compose
First, connect to your VPS via SSH. Then, install the Docker engine and Docker Compose plugin.
# Update package lists
sudo apt-get update
# Install prerequisite packages
sudo apt-get install -y ca-certificates curl
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the Docker repository to Apt sources
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Update package lists again with the new repository
sudo apt-get update
# Install Docker Engine and Docker Compose
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Part 2: Setting Up the Core Infrastructure (Traefik & Portainer)
We will create a foundational "infra" stack that runs Traefik (our reverse proxy) and Portainer (a web UI for managing Docker).
Step 2: Create the Directory Structure
Organizing our stacks into folders keeps everything clean.
# Create a main directory for all your container stacks
mkdir -p /root/stacks/infra
# Navigate into the new directory
cd /root/stacks/infra
Step 3: Create the Traefik Docker Compose File
Create a `docker-compose.yml` file for Traefik.
nano /root/stacks/infra/docker-compose.yml
Paste the following content into the file. Remember to replace `your-email@example.com`, `traefik.your-domain.com`, and `portainer.your-domain.com`. You will also need to generate a `htpasswd` for the dashboard and replace the placeholder.
version: '3.8'
services:
traefik:
image: "traefik:v3.0"
container_name: "traefik"
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true"
- "--certificatesresolvers.mytlschallenge.acme.email=your-email@example.com"
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- shared-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.your-domain.com`)"
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
- "traefik.http.routers.traefik-dashboard.tls.certresolver=mytlschallenge"
- "traefik.http.routers.traefik-dashboard.service=api@internal"
- "traefik.http.routers.traefik-dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$...$$" # Replace with generated htpasswd
portainer:
image: "portainer/portainer-ce:latest"
container_name: "portainer"
restart: unless-stopped
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "portainer_data:/data"
networks:
- shared-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.your-domain.com`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
- "traefik.http.routers.portainer.tls.certresolver=mytlschallenge"
networks:
shared-net:
name: shared-net
volumes:
portainer_data:
Tip: To generate the `htpasswd` needed for the `basicauth.users` middleware, you can use an online generator or install `apache2-utils` (`sudo apt-get install apache2-utils`) and run `htpasswd -nb admin your-secure-password`.
Step 4: Start the Infrastructure Stack
# From the /root/stacks/infra directory
docker compose up -d
Part 3: Deploying Your Application
Step 5: Create a Directory for Your App
mkdir -p /root/stacks/app/my-awesome-app
cd /root/stacks/app/my-awesome-app
Step 6: Create the Application's Docker Compose File
Create a `docker-compose.yml` in this new directory.
nano /root/stacks/app/my-awesome-app/docker-compose.yml
Paste and customize the following template:
version: '3.8'
services:
my-awesome-app-service:
image: your-dockerhub-username/your-app-image:1.0.0
container_name: my-awesome-app
restart: unless-stopped
networks:
- shared-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.my-awesome-app.rule=Host(`app.your-domain.com`)"
- "traefik.http.routers.my-awesome-app.entrypoints=websecure"
- "traefik.http.routers.my-awesome-app.tls.certresolver=mytlschallenge"
- "traefik.http.services.my-awesome-app.loadbalancer.server.port=8080"
networks:
shared-net:
external: true
Step 7: Create the Environment File (Required)
Tip: This step is mandatory. Your application service will fail to start without a correctly configured `.env` file next to your `docker-compose.yml`.
Create an `.env` file to store your application's secrets and environment variables.
nano /root/stacks/app/my-awesome-app/.env
Add your variables. For example:
DATABASE_USER=myuser
API_SECRET_KEY=supersecret
Next, ensure your `docker-compose.yml` references this file by adding `env_file: [".env"]` to your application service definition.
Finally, set strict permissions on the file so only the owner can read and write it.
chmod 600 /root/stacks/app/my-awesome-app/.env
Step 8: Start Your Application
# From the /root/stacks/app/my-awesome-app directory
docker compose up -d
Part 4: Creating a CI/CD Update Workflow (Optional)
Step 9: Create a GitHub Actions Workflow
On your local machine, create the file `.github/workflows/docker-publish.yml` in your project repo.
name: CI - Build and Push Docker Image
on:
release:
types: [published]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: $
password: $
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
$/your-app-name:latest
$/your-app-name:$
Step 10: Add Secrets to GitHub
In your GitHub repository, go to `Settings` > `Secrets and variables` > `Actions`. Add two secrets:
- `DOCKERHUB_USERNAME`
- `DOCKERHUB_TOKEN`
Step 11: Create a Dedicated Restart Script on the Server (Optional)
mkdir -p /root/scripts
nano /root/scripts/restart_my-awesome-app.sh
Paste the following:
#!/bin/bash
set -e
APP_DIR="/root/stacks/app/my-awesome-app"
echo "--- Stopping and removing the my-awesome-app stack... ---"
cd "$APP_DIR" && docker compose down
echo "--- Pulling the latest image... ---"
cd "$APP_DIR" && docker compose pull
echo "--- Starting the my-awesome-app stack... ---"
cd "$APP_DIR" && docker compose up -d
echo "--- Update complete. Verifying status... ---"
docker ps --filter "name=my-awesome-app"
Make it executable:
chmod +x /root/scripts/restart_my-awesome-app.sh
Part 5: The Full Update Process
- Local Machine: Push code changes to GitHub.
- GitHub: Create a new Release. This triggers the action to build and push the new Docker image.
- Server: After the action completes, SSH into your server.
- Server: Update the image tag in `/root/stacks/app/my-awesome-app/docker-compose.yml`.
- Server: Run the update script: `/root/scripts/restart_my-awesome-app.sh`.
Your updated application is now live.