Securely Self-Hosting Your Own Website Using Ghost
Let's say you want to make your own website. The easiest option would be to use a cloud-based service, which 99% of the time requires some sort of recurring payment to use. This can be very convenient as you can simply press a couple buttons, and then have your very own website. Most of the time these types of services even have tools you can use to easily customize your website to fit your needs.
However, what we often think of as "the cloud" is really just someone else's computer. You can never fully know what kind of access the cloud provider has to your data or how well they secure their servers. And on top of that you have to pay them money on a recurring basis! These are several reasons why I chose to self-host my own website (it's actually because I just wanted to and those other reasons are ways to justify that). I control my own website's security, I control my own data, and best of all is that I don't pay anyone anything for it!
I primarily used this guide as inspiration for this project, but in this guide I will elaborate on what they did based on my own implementation successes/failures, and elaborate in areas that I think will be useful for you.

Note: Ghost is an easy to use website application, however it's admin page is unchangeably set to "https://<your public domain>.com/ghost" and does not provide MFA support. This opens up risk to brute force attacks. To help mitigate this, we will use Authentik as an additional authentication method which will require visitors to have a web browser cookie that is received upon successful authentication with Authentik before accessing the Ghost admin page.
Security Advisory
"It is easy to forget that when you are plugged into the Internet, the Internet is also plugged into you"
Running any public service over the Internet opens yourself up to risk. The Internet is a network that spans the entire planet. This means that if your network is like your house, then being connected to the Internet not only invites any person around the world to knock on your door, but also to pick your locks or find other ways to get in to your home. On top of that - in the case of the Internet - it's not just people walking up to your door, but also bots, entire organizations, or even nation-state actors.
Everything is constantly being poked and pinged on the Internet. Even right now your own home network is constantly being bombarded with traffic from the outside, but it almost never gets through because your home network most likely has a firewall blocking traffic outside from coming in. Running a public service means opening a way from outside into your network which opens yourself up to the risk of malicious actors hacking your device(s), installing malware on your device(s), stealing data from your device(s), and/or permanently damaging your device(s).
In other words, if you host any public service (including a website), you are opening yourself up to risk no matter how much security you implement to protect yourself. If you don't have to, then you shouldn't open yourself up to this risk.
I am only a technology enthusiast and an aspiring cyber security professional. I am by no means a cyber security subject matter expert and am only creating this guide to showcase my own project with other technology enthusiasts.
Always remember that you can NEVER not be at risk of compromise when hosting anything publicly. It's never a matter of IF, but only a matter of WHEN. However, you can take certain steps to mitigate and minimize that risk, some of which will be discussed in this guide.
Installation Guide
Preparation
Before starting, it is important to make sure the network your web server will be hosted on is locked down and secure. It is ideal to have multiple mechanisms of blocking intruders and malicious actors, and multiple safety nets of security to slow down incoming threats. I would use the OSI model as a guideline to evaluate your layers of security coverage. I would even use the MITRE ATT&CK Framework to also help inform your security posture.
For example, place your web server in a DMZ or some other isolated network from the rest of your devices and set appropriate firewall rules to ensure nothing can communicate between your internal network(s) and the DMZ (bi-directional is best). Implement VLANs if needed to ensure layer 2 separation. Maybe even implement an IPS/IDS (Snort for example) that monitors incoming DMZ traffic for additional visibility and security, or a geo-blocker (pfBlocker for example) to block IP addresses from certain geographic areas to reduce potential attack vectors, or a DNS blackhole/sinkhole (pfBlocker or PiHole for example) to prevent DMZ devices from calling out to potentially malicious domains.
Determine if you have a static public IP or a dynamic public IP with your ISP. You will need to use a DDNS service if you don't have a static IP. DDNS (Dynamic Domain Name System) is a software that runs on your network to update a domain record with your IP address whenever it changes.
You will also need to purchase a wildcard public domain (wildcard meaning that you can use all it's subdomains). For example, if you bought "example.com", then a wildcard domain would look like "*.example.com" so you could use "auth.example.com" or "website.example.com".
You will need to create an account with Crowdsec and Tailscale.
CrowdSec Console Login/Signup Page
Tailscale Login/Signup Page
Domain Records and Initial Firewall Rules Configuration
Create an A record with your DNS provider pointing to your website if you have a static public IP. Or create DDNS record that your public domain will point to if not.
Create a subdomain A record for Authentik. It's your choice what you make it, but for this guide I will make it auth.<your public domain>.com
Keep it disabled for now, but configure port forwarding for port 443 and port 80 on your router's public IP address to the same ports on your Ubuntu Linux server. We will enable the port forward once the server is ready to accept the communication.
It is probably not needed, but I thought I would mention you may also need to temporarily add a firewall rule to your router to allow SSH to the Ubuntu Linux server from your internal network. This firewall rule should be removed once you are able to SSH into the virtual machine using Tailscale later in the guide.
Virtual Machine Install and Docker Setup
We will be using a fresh install of Ubuntu Linux running on a virtual machine server. It is best practice to check and make sure that the virtual machine can only communicate on the DMZ network and cannot communicate with the rest of your internal network.
Note: I recommend running all servers within a virtual machine if not explicitly told otherwise, because virtual machines are much easier to back up, revert, or rebuild in case of compromise (or in case you change something and you accidentally break the virtual machine).
Install Ubuntu Linux Server. Don't install additional software during the setup.

After installation, run an update command to install the latest security updates to your Ubuntu server:
$ sudo apt-get update && sudo apt-get upgrade
Ensure you don't have docker already installed. If so, uninstall docker.
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; doneNext, install Docker Engine + Docker Compose. I've taken excerpts from the guide and pasted them below for convenience, but I would highly recommend using the official guide instead in case anything changes or you run into trouble.

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
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
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get updatesudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginTest your docker install to ensure it's working:
sudo docker run hello-worldSet UFW Rules for Docker and Change Default SSH Port
Next, change your UFW config to block docker ports:
$ nano /etc/ufw/after.rules
Add the following to the end of the file:
# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:ufw-docker-logging-deny - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j ufw-user-forward
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16
-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
-A DOCKER-USER -j RETURN
-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] "
-A ufw-docker-logging-deny -j DROP
COMMIT
# END UFW AND DOCKERPress CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Restart the firewall service:
$ systemctl restart ufw
Next, change the Ubuntu SSH port to a random non-standard port of your choosing (for reference, the range of non-standard ports are 49152 - 65535). Edit the sshd_config file using the below command:
$ nano /etc/ssh/sshd_config
Uncomment #Port 22 and change 22 to your chosen non-standard port.
Press CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Next, change your SSH service file. Use the same non-standard port you previously chose:
$ nano /etc/services
Find the SSH service and set to the previously specified non-standard port.
Press CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Reboot the machine. If you were previously using SSH to remotely connect to the machine, you must now connect to it by using the below command:
$ ssh -p <non-standard port> <username>@<Ubuntu IP Address>
Configure Tailscale VPN
If you haven't already done so, Create Tailscale account on their website.
Install Tailscale VPN. I've taken excerpts from the guide and pasted them below for your convenience, but I would highly recommend using the official guide in case anything changes or you run into trouble.

On your own device, run:
$ curl -fsSL https://tailscale.com/install.sh | sh
$ tailscale up
Follow the on screen instructions until complete.
Repeat the above commands on the Ubuntu Linux server as well. Once complete, you should have Tailscale installed on both your Ubuntu Linux server and your personal device which joins them to the same Tailscale network.
Next, run this command on your Ubuntu server:
$ tailscale ip -4
This will output the Tailscale IP of the Ubuntu server. Now SSH into the Ubuntu server using the Tailscale IP to ensure it works. Only proceed with the instructions once this works:
$ ssh -p <non-standard port> <username>@<tailscale server IP>
Lock SSH Access Behind Tailscale VPN
On your Ubuntu Linux server, input the below firewall configuration commands:
$ ufw allow in on tailscale0
$ ufw allow http
$ ufw allow https
Next, confirm your firewall rules allow HTTP, HTTPS, and tailscale0:
$ ufw show added
Once confirmed, enable ufw:
$ ufw enable
To summarize, the above steps will set your server to accept direct connections on port 80 and 443, and will set the server to allow all incoming connections using Tailscale (which includes SSH using the non-standard port set earlier).
Note: If you configured a firewall rule to allow SSH from your internal network to your DMZ, you can now delete that firewall rule since you should be able to SSH into your Ubuntu server using Tailscale.
Create Docker User, Docker Group, and Docker Directory
The below commands will create a group for running docker commands if not already made, create a user called "ghostuser" for running the docker containers, create a directory to store the docker-compose.yml file, modify the permissions for the directory to be owned by ghostuser, and add ghostuser to the docker group. This is done so that if the ghostuser account is compromised, the user cannot modify any directories other than the ones it already owns:
$ groupadd docker
$ adduser ghostuser
$ mkdir /opt/ghost
$ mkdir /opt/npm
$ chown ghostuser:ghostuser /opt/ghost
$ chown ghostuser:ghostuser /opt/npm
$ usermod -aG docker ghostuser
Create The Initial Docker-Compose File
First we will start with creating the NPMPlus & Crowdsec containers. We will be following the NPMPlus guide for initial NPMPlus set up. Then, we will use the Crowdsec section of the guide to add a web application firewall to the website. I've taken excerpts from the guide and pasted them below for your convenience, but I would highly recommend using the official guide in case anything changes or you run into trouble.
NPMPlus Install Section of Guide
CrowdSec Enablement Section of Guide
Enter the below commands to create the docker-compose.yml file:
$ su ghostuser
$ cd /opt/npm
$ nano docker-compose.yml
Copy and paste the contents of the NPMPlus Docker compose file into your docker-compose.yml file.
Uncomment the CrowdSec portion of the docker-compose file, but leave the openappsec line commented. Adjust the TZ values to your tz database timezone and ACME_EMAIL to your email address.
Press CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Next, start your containers:
$ docker compose up -d
Check the status of your docker containers using the below command (keep this handy for the rest of the guide):
$ docker ps
Wait until your docker contains are done pulling and initializing. Next, check the logs for your npmplus container to find your temporary web admin password:
$ docker logs npmplus
Look for the password in the log output, then in your web browser go to the https://<tailscale IP>:81 and log in with default username of [email protected] and previously found password. You will be prompted to change your password. Once complete, bring your containers down in your SSH session:
$ docker compose down
Next edit the docker-compose.yml file again:
$ nano docker-compose.yml
Uncomment the LOGROTATE line.
Press CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Now start your containers up again:
$ docker compose up -d
Once back up, edit the the npmplus.yaml file and fill it with the below output:
$ nano /opt/crowdsec/conf/acquis.d/npmplus.yaml
filenames:
- /opt/npmplus/nginx/*.log
labels:
type: npmplus
---
filenames:
- /opt/npmplus/nginx/*.log
labels:
type: modsecurity
---
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/appsec-default
name: appsec
source: appsec
labels:
type: appsec
# if you use openappsec you can enable this
#---
#source: file
#filenames:
# - /opt/openappsec/logs/cp-nano-http-transaction-handler.log*
#labels:
# type: openappsecPress CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Next, find your API key for crowdsec by using the below command. Make sure to keep it handy:
$ docker exec crowdsec cscli bouncers add npmplus -o raw
Edit your crowdsec configuration file:
$ nano /opt/npmplus/crowdsec/crowdsec.conf
Set ENABLED to true
Set API_KEY to the previously saved output
Press CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Restart your docker containers.
$ docker compose down
$ docker compose up -d
Enroll Your Crowdsec Container Into Crowdsec Security Engine
Follow the Crowdsec post-install instructions to enroll the Crowdsec docker container in Crowdsec Security Engine.

For when you get to the Enrollment Key part of the guide, modify the given command like below:
$ docker exec crowdsec cscli console enroll <enrollment key>
Once complete, refresh the Engines page and accept the new Engine.
You can use the Crowdsec console to monitor what traffic Crowdsec is blocking. Do note that while Crowdsec console has basic functionality for free, you may need to pay to use additional features. For example, sometimes Crowdsec may lock you out of your own web server if you are messing with HTML. If you pay for Crowdsec, you can unblock your IP address directly from the security console. If you don't, you'll have to SSH into your web server and execute a command in your Crowdsec container to unblock yourself. You can find that command below for your convenience:
$ docker exec crowdsec cscli decisions delete --ip <your public ip address>
Create Your NPMPlus Hosts
Go back to your NPMPlus web admin page at https://<tailscale IP>:81
Next, create a proxy host for your previously created authentication subdomain:
Set the scheme to HTTPS, domain/ip to 127.0.0.1, port to 9443.
Enable Websockets support.
Set TLS to Create new Certificate for your base public domain.
Check Force HTTPS, Enable HSTS and security headers, Enable Brotli, Enable HTTP/3-Quic.
Click Save.
Next, create a proxy host for your base public domain:
Set scheme to HTTP, domain/ip to 127.0.0.1, port to 8081.
Enable Websockets support.
Set TLS to use your previously created certificate.
Check Force HTTPS, Enable HSTS and security headers, Enable Brotli, Enable HTTP/3-Quic.
Under the Advanced tab, enter the below text and then click Save.
location ~ ^/ghost/api/ {
proxy_pass $forward_scheme://$server:$port;
proxy_set_header Host $host;
# Add CORS headers for API
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
}
# Main location block for Ghost admin panel that requires authentication
location ~ ^/ghost/ {
# Exclude /ghost/api from authentication
location ~ ^/ghost/api/ {
proxy_pass $forward_scheme://$server:$port;
proxy_set_header Host $host;
# Add any other required proxy headers for Ghost API
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
# Authentication for all other /ghost/ paths
proxy_pass $forward_scheme://$server:$port;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
##############################
# authentik-specific config
##############################
auth_request /outpost.goauthentik.io/auth/nginx;
error_page 401 = @goauthentik_proxy_signin;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
# translate headers from the outposts back to the actual upstream
auth_request_set $authentik_username $upstream_http_x_authentik_username;
auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
auth_request_set $authentik_email $upstream_http_x_authentik_email;
auth_request_set $authentik_name $upstream_http_x_authentik_name;
auth_request_set $authentik_uid $upstream_http_x_authentik_uid;
proxy_set_header X-authentik-username $authentik_username;
proxy_set_header X-authentik-groups $authentik_groups;
proxy_set_header X-authentik-email $authentik_email;
proxy_set_header X-authentik-name $authentik_name;
proxy_set_header X-authentik-uid $authentik_uid;
}
# Root location for all other paths
location / {
proxy_pass $forward_scheme://$server:$port;
proxy_set_header Host $host;
}
# Authentik outpost configuration
location /outpost.goauthentik.io {
proxy_pass https://localhost:9443/outpost.goauthentik.io;
proxy_set_header Host $host;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
add_header Set-Cookie $auth_cookie;
auth_request_set $auth_cookie $upstream_http_set_cookie;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
# Authentication redirect handler
location @goauthentik_proxy_signin {
internal;
add_header Set-Cookie $auth_cookie;
return 302 $scheme://$http_host/outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri;
}The above steps tells NPMPlus to direct requests for your authentication subdomain (we are using auth.<your public domain>.com) to port 9443 on your Ubuntu server, which is the port that Authentik will run on. Then it tells NPMPlus to direct requests for your public domain to port 8081 on your Ubuntu server, which is the port that Ghost will run on. It also adds additional reverse proxy configs to lock the Ghost admin page behind authentication with Authentik.
Set Up Your Authentik Container
Shut down your docker containers, then edit the docker-compose.yml file.
$ docker compose down
$ nano /opt/ghost/docker-compose.yml
Append the below text to the end of the file:
postgresql:
env_file:
- .env
environment:
POSTGRES_DB: ${PG_DB:-authentik}
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
healthcheck:
interval: 30s
retries: 5
start_period: 20s
test:
- CMD-SHELL
- pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
timeout: 5s
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
volumes:
- database:/var/lib/postgresql/data
server:
command: server
depends_on:
postgresql:
condition: service_healthy
env_file:
- .env
environment:
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.0}
ports:
- ${COMPOSE_PORT_HTTP:-9000}:9000
- ${COMPOSE_PORT_HTTPS:-9443}:9443
restart: unless-stopped
volumes:
- ./media:/media
- ./custom-templates:/templates
worker:
command: worker
depends_on:
postgresql:
condition: service_healthy
env_file:
- .env
environment:
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.0}
restart: unless-stopped
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
volumes:
database:
driver: localPress CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Enter the below commands to generate passwords and change some values for your .env file:
$ echo "PG_PASS=$(openssl rand -base64 36 | tr -d '\n')" >> .env
$ echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n')" >> .env
$ echo "AUTHENTIK_ERROR_REPORTING__ENABLED=true" >> .env
Next, start up your docker containers again:
$ docker compose up -d
If you already set up your port forward from your router's public IP to your Ubuntu server, you are now ready to enable it and will need to have that working before proceeding.
Once the containers are fully up and the port forward is working, manage your authentik page by going to https://auth.<your public domain>.com/if/flow/initial-setup/
Set a new password when prompted. Once done, you should be at the Authentik admin page.
Set the Output settings by going to Applications > Outposts. Make sure "authentik_host" is set to https://auth.<your public domain>.com
Set the Provider settings by going to Applications > Providers > Create > Proxy Provider
Set the Provider name to something like "ghost" (the name is up to you. just make sure you remember it).
Select Forward auth (domain level)
Set "Authentication URL" to https://auth.<your public domain>.com
Set Cookie Domain to <your public domain>.com
Open "Advanced Protocol Settings" and add the below to "Unauthenticated URLs"
^/ghost/api/.*
^/members/.*
^/sitemap.xml
^/robots.txtNext, create the Application by going to Applications > Applications. Set the name to something like "ghost" (the name is up to you. just make sure you remember it), then set the provider to previously created provider.
Go to the Outposts tab again and assign the application to the outpost.
I would also highly recommend changing your admin username to something other than "akadmin", and adding some sort of MFA to your account. Change the name by going to Directory > Users > click "Edit" next to the admin user > change the username. Set up MFA by clicking the gear icon in the top-right > MFA Devices > Enroll. I'd recommend enrolling a TOTP device so you can use an authenticator app on your phone as MFA.
Set Up Your Ghost Container
Bring your docker containers down once more, then edit your docker-compose.yml file.
$ docker compose down
$ nano docker-compose.yml
Add the below text to the docker-compose file. Make sure you change https://<your public domain>.com to your public domain:
version: "3.3"
services:
ghost-frontend:
image: nginx:1.15-alpine
container_name: ghost-frontend
restart: unless-stopped
volumes:
- ./nginx/ghost.conf:/etc/nginx/conf.d/ghost.conf
ports:
- 8081:8081
depends_on:
- ghost-app
networks:
- ghost
ghost-app:
container_name: ghost-app
image: ghost:latest
restart: always
depends_on:
- ghost-mysql
environment:
url: https://<your public domain>.com
database__client: mysql
database__connection__host: ghost-mysql
database__connection__user: mysql-user
database__connection__password: mysql-password
database__connection__database: ghost
volumes:
- ./content:/var/lib/ghost/content
networks:
- ghost
ghost-mysql:
container_name: ghost-mysql
image: mysql:8
restart: always
environment:
MYSQL_ROOT_PASSWORD: mysql-root-password
MYSQL_USER: mysql-user
MYSQL_PASSWORD: mysql-password
MYSQL_DATABASE: ghost
volumes:
- ./mysql8:/var/lib/mysql
networks:
- ghost
networks:
ghost:
name: ghost
driver: bridgePress CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Next, create an "nginx" directory and then create a config file within. Copy the below text and paste it into your ghost.conf file, but make sure to change <your public domain> to your domain that Ghost will run on.
$ mkdir nginx
$ nano /opt/ghost/nginx/ghost.conf
server {
listen 8081;
server_name www.<your public domain>.com;
return 301 $scheme://<your public domain>$request_uri;
}
server {
listen 8081;
server_name <your public domain>.com;
location / {
proxy_pass http://ghost-app:2368;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
# The lines below are commented out for compatibility due to redirect errors when adding a another reverse proxy in fr>
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
}
}Press CTRL+O to save, ENTER to confirm the file name, then CTRL+X to exit nano.
Next, bring your docker containers back up.
$ docker compose up -d
Ensure docker containers are all running without issue:
$ docker ps
Test
Open an incognito/private window in your web browser, then go to your website. Your website should load page with HTTPS and you should see your certificate upon inspection.
Next, test the Authentik admin page by going to https://<your public domain>.com/ghost
You should be automatically redirected to https://auth.<your public domain>.com
Log in with your previously created authentik admin credentials.
Upon success you should be redirected to the regular ghost web admin page.
From here, create a web admin password for Ghost. Once complete, you are now all set and free to set up your website.
Maintenance
It is important to review any servers exposed to the Internet and ensure they are not missing any security updates, the logs look okay, and that your server is being backed up. In this case, we would want to make sure Ubuntu has the latest security updates, all the docker containers are up to date (especially Crowdsec), the log activity looks okay, and that there is a good backup plan in place.
Backing Up Your Server
If you can, try to follow the 3-2-1 backup rule. This means that you are keeping at least 3 copies of your data across 2 different storage media with 1 being located off site. Even if you can't achieve this, at least make sure you keep a backup of your data on a separate media (cloud storage may be handy for this if you encrypt the data backup).
To back up your Ghost data, you can follow this guide:
Ubuntu Security Updates
Before installing updates of ANY kind, always make sure you have a backup readily available. Virtual machine snapshots are a handy way to quickly make a "save point" for you to return to in case security updates break anything.
To install the latest security updates, SSH into your Ubuntu server, then run the below command:
$ sudo apt-get update && sudo apt-get upgrade
You may also need to reboot the server once security updates are completed. You will be prompted to do so after updates are completed.
Docker Container Updates
Just wanted to mention one more time: before installing updates of ANY kind, always make sure you have a backup readily available. Again, a virtual machine snapshot is a handy way to have a "save point" to return to in case updating your docker containers breaks your virtual machine.
To install the latest Docker container images, SSH into your Ubuntu server, then run the below command:
$ su ghostuser
$ cd /opt/ghost && docker compose down && docker compose pull && docker compose up -d
$ cd /opt/npm && docker compose down && docker compose pull && docker compose up -d
The below command is useful after updating your containers as it may clean up some storage space. Make sure your containers are all running before using it as it deletes containers that are not currently in use:
$ docker container prune
Review Log Activity
In a nutshell, Docker containers run as users on your host system. If exploited, then attackers can run commands as the user the docker containers are running as. Checking command logs, successful logins, and service starts/stops are good starting points for ensuring your server is safe. The below guide is a useful starting point that I found to monitor your server logs.

You can also review logs on your Crowdsec Engine web page. There you can see all kinds of security information directly related to your web server.
Other than that, this is where deploying security tools would be useful to further enhance the activity you are able to see and the corrective actions you are able to perform.




