One-Command Server Setup with Ansible: Nginx + Docker + Certbot + Django/React
This guide shows an opinionated, minimal Ansible layout to turn a fresh Linux VM into a production‑ish web host:
- Installs system tools (batcat, ncdu, trash-cli)
- Installs Docker and Docker Compose (plugin)
- Deploys two containers: Django API and React frontend
- Installs and configures Nginx as a reverse proxy
- Issues HTTPS certificates via Certbot and reloads Nginx
Assumptions:
- Your domain points to the server’s public IP (A/AAAA records)
- You have SSH access as a sudo‑capable user
- You have container images for Django and React (or use the examples)
0) Install Ansible on your laptop
Pick one method:
# Using pipx (recommended)
pipx install ansible
# Or on Debian/Ubuntu
sudo apt update && sudo apt install -y ansible
# Verify
ansible --version
1) Project structure
Create a folder like infra/ with the following skeleton:
infra/
inventory/
hosts.ini
group_vars/
prod.yml
roles/
common/
tasks/main.yml
docker/
tasks/main.yml
app/
templates/docker-compose.yml.j2
tasks/main.yml
nginx/
templates/site.conf.j2
tasks/main.yml
certbot/
tasks/main.yml
site.yml
inventory/hosts.ini
[prod]
your.server.fqdn ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa
group_vars/prod.yml
domain: example.com
admin_email: admin@example.com
app_root: /srv/webapp
# Published ports for host Nginx to proxy to
react_host: 127.0.0.1
react_port: 3000
django_host: 127.0.0.1
django_port: 8000
# Container images (replace with your own)
react_image: ghcr.io/example/react-frontend:latest
django_image: ghcr.io/example/django-api:latest
# Optional env
django_env:
DJANGO_SETTINGS_MODULE: app.settings
SECRET_KEY: changeme
ALLOWED_HOSTS: "{{ domain }}"
DB_HOST: 127.0.0.1
DB_NAME: app
DB_USER: app
DB_PASSWORD: secret
2) site.yml (the playbook)
---
- name: Provision web host
hosts: prod
become: true
vars:
ansible_python_interpreter: /usr/bin/python3
roles:
- common
- docker
- app
- nginx
- certbot
3) Role: common (tools and basics)
roles/common/tasks/main.yml
---
- name: Update apt cache
apt:
update_cache: true
- name: Install useful CLI tools
apt:
name:
- bat
- ncdu
- trash-cli
- ca-certificates
- curl
- gnupg
state: present
- name: Create app root
file:
path: "{{ app_root }}"
state: directory
mode: '0755'
Note: On Debian bullseye/bookworm, bat is installed as the batcat binary.
4) Role: docker (Docker Engine + Compose plugin)
roles/docker/tasks/main.yml
---
- name: Add Docker’s official GPG key
apt_key:
url: https://download.docker.com/linux/debian/gpg
state: present
when: ansible_facts['os_family'] == 'Debian'
- name: Add Docker repository
apt_repository:
repo: "deb [arch={{ ansible_architecture }}] https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }} {{ ansible_facts['distribution_release'] }} stable"
state: present
when: ansible_facts['os_family'] == 'Debian'
- name: Install Docker Engine and Compose plugin
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
update_cache: true
- name: Ensure Docker service is running
service:
name: docker
state: started
enabled: true
5) Role: app (docker-compose stack for React + Django)
roles/app/templates/docker-compose.yml.j2
version: '3.8'
services:
react:
image: {{ react_image }}
restart: unless-stopped
ports:
- "{{ react_host }}:{{ react_port }}:3000"
environment:
- NODE_ENV=production
django:
image: {{ django_image }}
restart: unless-stopped
environment:
- DJANGO_SETTINGS_MODULE={{ django_env.DJANGO_SETTINGS_MODULE }}
- SECRET_KEY={{ django_env.SECRET_KEY }}
- ALLOWED_HOSTS={{ django_env.ALLOWED_HOSTS }}
- DB_HOST={{ django_env.DB_HOST }}
- DB_NAME={{ django_env.DB_NAME }}
- DB_USER={{ django_env.DB_USER }}
- DB_PASSWORD={{ django_env.DB_PASSWORD }}
command: ["gunicorn", "app.wsgi:application", "-b", "0.0.0.0:8000", "-w", "3"]
ports:
- "{{ django_host }}:{{ django_port }}:8000"
roles/app/tasks/main.yml
---
- name: Copy docker-compose file
copy:
dest: "{{ app_root }}/docker-compose.yml"
content: "{{ lookup('template', 'docker-compose.yml.j2') }}"
- name: Start app stack
community.docker.docker_compose_v2:
project_src: "{{ app_root }}"
state: present
Tip: Install community.docker collection once on your control machine: ansible-galaxy collection install community.docker.
6) Role: nginx (reverse proxy)
roles/nginx/templates/site.conf.j2
server {
listen 80;
server_name {{ domain }};
# Let’s Encrypt HTTP-01 challenge
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
# React frontend
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://{{ react_host }}:{{ react_port }};
}
# Django API
location /api/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://{{ django_host }}:{{ django_port }};
}
}
roles/nginx/tasks/main.yml
---
- name: Install nginx
apt:
name: nginx
state: present
- name: Create webroot for ACME challenges
file:
path: /var/www/letsencrypt
state: directory
owner: www-data
group: www-data
mode: '0755'
- name: Deploy site config
copy:
dest: "/etc/nginx/sites-available/{{ domain }}.conf"
content: "{{ lookup('template', 'site.conf.j2') }}"
- name: Enable site
file:
src: "/etc/nginx/sites-available/{{ domain }}.conf"
dest: "/etc/nginx/sites-enabled/{{ domain }}.conf"
state: link
- name: Remove default site if present
file:
path: /etc/nginx/sites-enabled/default
state: absent
- name: Test nginx config
command: nginx -t
register: nginx_test
changed_when: false
- name: Reload nginx
service:
name: nginx
state: reloaded
7) Role: certbot (issue TLS and reload nginx)
roles/certbot/tasks/main.yml
---
- name: Install Certbot for Nginx
apt:
name:
- certbot
- python3-certbot-nginx
state: present
update_cache: true
- name: Obtain/renew certificate (nginx plugin)
command: >-
certbot --nginx -d {{ domain }}
--non-interactive --agree-tos -m {{ admin_email }} --redirect
args:
creates: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem"
- name: Ensure systemd timer for renewals is active
service:
name: certbot.timer
state: started
enabled: true
This uses the Nginx plugin to edit the HTTP vhost and add HTTPS + automatic redirection. If you prefer an immutable Nginx config, switch to the --webroot method and add an HTTPS server block to the template.
8) Run it
cd infra
ansible-playbook -i inventory/hosts.ini site.yml
When it finishes, visit https://{{ domain }} — the request hits host Nginx on 443, which proxies / to the React container and /api/ to the Django container. Certificates renew automatically.
Notes and tweaks
- Build pipelines: push your Django and React images to a registry (GHCR, Docker Hub) and deploy by tag.
- Static React: If your React image already serves static files via an embedded Nginx, the proxy works the same.
- Healthchecks: add
healthcheckto services andlocation /healthzin Nginx if useful. - Logs: use
docker logsor route to a collector (Loki, etc.). - Firewalls: open 80/443 only; block 3000/8000 from the internet by binding to 127.0.0.1 (as above).
That’s it — a tidy Ansible starter you can extend with roles for Postgres, Redis, or metrics.