<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lukas Mauser</title>
    <description>The latest articles on DEV Community by Lukas Mauser (@wimadev).</description>
    <link>https://dev.to/wimadev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1134748%2F7788db9e-91c1-44a6-93d1-28d6bec76793.jpeg</url>
      <title>DEV Community: Lukas Mauser</title>
      <link>https://dev.to/wimadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wimadev"/>
    <language>en</language>
    <item>
      <title>Deploy Django on a VPS with Docker: Step-by-Step Guide</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Fri, 26 Dec 2025 16:54:21 +0000</pubDate>
      <link>https://dev.to/wimadev/deploy-django-on-a-vps-with-docker-step-by-step-guide-21e2</link>
      <guid>https://dev.to/wimadev/deploy-django-on-a-vps-with-docker-step-by-step-guide-21e2</guid>
      <description>&lt;p&gt;Deploying a Django application on a Virtual Private Server (VPS) is a cheap, privacy friendly and flexible way to host your projects. In this guide, we'll walk you through the process of deploying a Django app on a VPS using Docker. We'll cover everything from setting up your Django app for Docker, choosing a VPS provider, configuring the server, and deploying the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is this guide for?
&lt;/h2&gt;

&lt;p&gt;This guide is for you if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;are curious about self-hosting and want to learn some fundamentals&lt;/li&gt;
&lt;li&gt;want to run small projects or side projects&lt;/li&gt;
&lt;li&gt;want to save money on hosting costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide is &lt;em&gt;not&lt;/em&gt; for you if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;are looking for the absolute fastest way to get started&lt;/li&gt;
&lt;li&gt;want to avoid ongoing maintenance&lt;/li&gt;
&lt;li&gt;need to run mission-critical workloads and don't know what you're doing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a simple, managed way to get started, check out our product: &lt;a href="https://sliplane.io?utm_source=django-vps" rel="noopener noreferrer"&gt;sliplane.io&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Django application, ready to be deployed&lt;/li&gt;
&lt;li&gt;An account on &lt;a href="https://hetzner.com" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt; (or other your preferred VPS provider)&lt;/li&gt;
&lt;li&gt;A domain name&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In a nutshell
&lt;/h2&gt;

&lt;p&gt;We will use Docker to containerize our Django app and Postgres as a database. Then, we'll choose a VPS provider, set up the server, and deploy the app using Docker Compose and a reverse proxy (Caddy) for SSL termination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerize the Django app
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Docker?
&lt;/h3&gt;

&lt;p&gt;Docker makes it easy to run custom software in the cloud. You can define everything your Django app needs in a &lt;code&gt;Dockerfile&lt;/code&gt; and then spin up the app in an isolated container with all dependencies installed.&lt;/p&gt;

&lt;p&gt;For example, our Django app requires a specific version of Python and a web server (like Gunicorn) to handle requests. We can define all of that in a &lt;code&gt;Dockerfile&lt;/code&gt; and then build an image from it. Once the image is built, we can run it on our server.&lt;/p&gt;

&lt;p&gt;We can also run a Postgres database in another container and connect our Django app to it without running into dependency conflicts. Docker gives you access to a huge ecosystem of prebuilt images for databases, caches, web servers, and much more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up Docker for Django
&lt;/h3&gt;

&lt;p&gt;If you want a detailed introduction, check out my &lt;a href="https://sliplane.io/blog/how-to-get-started-with-django-in-docker" rel="noopener noreferrer"&gt;Django in Docker tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Create two files named &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt; in the root of your Django project and add the following content:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dockerfile:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Base build stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.13-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="c"&gt;# Create the app directory&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; /app

&lt;span class="c"&gt;# Set the working directory&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Set environment variables to optimize Python&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1 &lt;/span&gt;

&lt;span class="c"&gt;# Upgrade pip and install dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip 

&lt;span class="c"&gt;# Copy the requirements file first (better caching)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt /app/&lt;/span&gt;

&lt;span class="c"&gt;# Install Python dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Stage 2: Production stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.13-slim&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; appuser &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;   &lt;span class="nb"&gt;mkdir&lt;/span&gt; /app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;   &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; appuser /app

&lt;span class="c"&gt;# Copy the Python dependencies from the builder stage&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /usr/local/bin/ /usr/local/bin/&lt;/span&gt;

&lt;span class="c"&gt;# Set the working directory&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy application code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser . .&lt;/span&gt;

&lt;span class="c"&gt;# Set environment variables to optimize Python&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1 &lt;/span&gt;

&lt;span class="c"&gt;# Switch to non-root user&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;

&lt;span class="c"&gt;# Expose the application port&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000 &lt;/span&gt;

&lt;span class="c"&gt;# Start the application using Gunicorn&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "myproject.wsgi:application"]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;docker-compose.yml:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test locally, make sure Docker is installed and running. &lt;a href="https://docs.docker.com/engine/install/" rel="noopener noreferrer"&gt;Install Docker&lt;/a&gt;.&lt;br&gt;
You can test locally if everything works by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spins up the django app and a postgres database. You can access the django app at &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;. Before you deploy your Django app, make sure everything works locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and push the Django Docker image
&lt;/h2&gt;

&lt;p&gt;The compose file above is great for local development, but not ideal for production, because it requires the server to build the image every time a new version is deployed. In production, you want to prebuild your Django app and store it in a container registry, so your server can simply pull the image and run it (just like the Postgres container).&lt;/p&gt;

&lt;p&gt;You can build your image using this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-django-app &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the image is build, we will push it to a container registry - a warehouse for Docker images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push to a container registry:&lt;/strong&gt;&lt;br&gt;
I recommend &lt;a href="https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry" rel="noopener noreferrer"&gt;GitHub Container Registry (GHCR)&lt;/a&gt; because it's free for private images. To get started, create a personal access token in GitHub first.&lt;/p&gt;

&lt;p&gt;Make sure the token has &lt;code&gt;write:packages&lt;/code&gt;, &lt;code&gt;read:packages&lt;/code&gt;, and &lt;code&gt;delete:packages&lt;/code&gt; scopes. &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token" rel="noopener noreferrer"&gt;See GitHub's docs for details&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can then use this token to authenticate which is required to push images to your private workspace:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authenticate with GHCR:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$GITHUB_TOKEN&lt;/span&gt; | docker login ghcr.io &lt;span class="nt"&gt;-u&lt;/span&gt; YOUR_GITHUB_USERNAME &lt;span class="nt"&gt;--password-stdin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tag and push your image:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker tag my-django-app ghcr.io/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:latest
docker push ghcr.io/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we use the &lt;code&gt;latest&lt;/code&gt; tag for simplicity, but you can use semantic versioning or git commit hashes for better version control and rollbacks.&lt;/p&gt;

&lt;p&gt;Every time you make changes to your Django app, rebuild and push the image. You might see how this can get tedious with frequent deployments, which is why these steps are often automated in a CI/CD pipeline. I won't go into the details in this tutorial, but I created a seperate post about how to &lt;a href="https://sliplane.io/blog/building-a-pipeline-to-deploy-docker-containers-to-a-vps" rel="noopener noreferrer"&gt;create a CI/CD pipeline with GitHub Actions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When the image is pushed, it's time to set up the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a VPS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. What VPS to choose?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We compared some popular providers in &lt;a href="https://sliplane.io/blog/top-5-cheap-vps-providers" rel="noopener noreferrer"&gt;this post on the top 5 cheap VPS providers&lt;/a&gt; and have had great experiences with Hetzner. They're affordable, reliable, and we've deployed thousands of servers with them. If you use our &lt;a href="https://hetzner.cloud/?ref=mZziDsGU2VVp" rel="noopener noreferrer"&gt;Hetzner affiliate link&lt;/a&gt;, you'll get €20 in credit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. How big does my VPS need to be?&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Start with the smallest option available and scale up as needed. You'd be surprised how far a basic VPS can get you — a Postgres database and a small Django app can run on as little as 1GB of RAM. You can always resize your server later. Start simple: run both the database and Django app on the same server (&lt;a href="https://sliplane.io/blog/deploy-multiple-apps-on-a-single-vps-with-docker" rel="noopener noreferrer"&gt;see our guide on running multiple apps on a single VPS&lt;/a&gt;). Only worry about more complex setups when you actually need to scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Where do I want my VPS to be located?&lt;/strong&gt;&lt;br&gt;
Choose a location close to your users for best performance. If latency isn't a big concern, pick the cheapest location. Prices vary due to electricity, taxes, etc. Hetzner, for example, has data centers in Germany and Finland that are usually cheaper than other locations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. What processor architecture - x86 or arm?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Arm is getting more popular because it's cheaper, but we've run into availability issues and some software compatibility problems. We usually stick with x86. If you develop on x86 and deploy on arm, you might hit unexpected issues. The good thing about VPSs: you only pay for the time your server runs, so you can test an arm server for a few cents and switch back to x86 if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Shared or dedicated?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start with shared and upgrade if necessary. Shared is generally slower and can sometimes be unpredictable, so provider choice matters. With Hetzner, we've had good experiences and shared server performance has been consistent and reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. What OS to choose?&lt;/strong&gt;&lt;br&gt;
Linux is the way to go for servers: it's free, secure, and reliable. The most popular distributions for servers are Ubuntu, Debian, CentOS, Fedora, and Arch Linux.&lt;/p&gt;

&lt;p&gt;I use Ubuntu because that's what I learned on—use the latest LTS version available. Differences between distributions are mostly a matter of preference. If you're unsure, go with Ubuntu. If you have specific requirements, you'll know what to do.&lt;/p&gt;

&lt;p&gt;In general, stick with what's popular so you can find help online when needed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting up the server
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Connect to your server
&lt;/h3&gt;

&lt;p&gt;After your server is set up and running, connect to it via SSH. By default, most providers give you a root user and password, but you can often provide your SSH public key during setup for more security.&lt;/p&gt;

&lt;p&gt;Open a terminal on your local machine and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@your-server-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;your-server-ip&lt;/code&gt; with the IP address of your server. You can usually find it in your VPS provider's dashboard or in a welcome email.&lt;/p&gt;

&lt;p&gt;If you're new to SSH, check out this guide from github about how you can &lt;a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent" rel="noopener noreferrer"&gt;generate ssh keys and add them to your agent&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardening the server
&lt;/h3&gt;

&lt;p&gt;Hardening means making it harder for attackers to compromise your server. This should be the first thing you do after setup. Measures include disabling root login, using SSH keys instead of passwords, setting up a firewall, keeping your system updated, using fail2ban, etc. It's good practice to use a hardening script so you don't forget anything important. I don't want to provide an outdated script here, but you can find plenty of good hardening scripts online or write your own. If you use a script from the internet, make sure you understand what it does and trust the source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firewall
&lt;/h3&gt;

&lt;p&gt;Hetzner offers a simple firewall you can configure via their dashboard. Only allow traffic on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS).&lt;/p&gt;

&lt;p&gt;It's recommended to use the Hetzner firewall in addition to a local firewall like UFW or iptables, since it blocks traffic before it reaches your server. This saves resources and adds an extra layer of protection (especially since Docker can sometimes bypass local firewalls).&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Docker
&lt;/h3&gt;

&lt;p&gt;Follow the &lt;a href="https://docs.docker.com/engine/install/" rel="noopener noreferrer"&gt;official Docker install guide&lt;/a&gt; to install Docker on your server.&lt;/p&gt;

&lt;p&gt;Now we're ready to deploy our Django app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run the django app on your server
&lt;/h3&gt;

&lt;p&gt;We'll use Docker Compose again to run both the Django app and the Postgres database. However, we need to modify the compose file slightly for production.&lt;/p&gt;

&lt;p&gt;First, set up your environment variables. You can use &lt;a href="https://docs.docker.com/engine/swarm/secrets/" rel="noopener noreferrer"&gt;Docker secrets&lt;/a&gt; or &lt;code&gt;.env&lt;/code&gt; files to store sensitive data like database passwords. For a simple setup, create a &lt;code&gt;.env&lt;/code&gt; file in the project directory on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mydb
&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myuser
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mypassword
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres://myuser:mypassword@db:5432/mydb
&lt;span class="nv"&gt;DJANGO_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-secret-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're a bit more serious about security, consider using Docker secrets instead. &lt;/p&gt;

&lt;p&gt;Here are 2 changes we need to make to the &lt;code&gt;docker-compose.yml&lt;/code&gt; for production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Change the &lt;code&gt;web&lt;/code&gt; service to pull the prebuilt image from your container registry instead of building from source. We'll also introduce a third service: a reverse proxy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a reverse proxy service. The reverse proxy is necessary in production, because it handles SSL termination and forwards requests to your Django app running in Docker. Most reverse proxies can also do some other cool tricks, like caching requests, compressing responses, and more. I compared 5 different reverse proxies in &lt;a href="https://sliplane.io/blog/top-5-reverse-proxies-which-one-should-you-choose" rel="noopener noreferrer"&gt;this post&lt;/a&gt;. One of the simplest is &lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt;, which comes with automatic SSL via Let's Encrypt and is very easy to configure.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your final &lt;code&gt;docker-compose.yml&lt;/code&gt; will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;

  &lt;span class="na"&gt;caddy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2.10.2&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;cap_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NET_ADMIN&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PWD/conf:/etc/caddy&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_config:/config&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To &lt;a href="https://hub.docker.com/_/caddy" rel="noopener noreferrer"&gt;configure Caddy&lt;/a&gt;, create a &lt;code&gt;conf&lt;/code&gt; directory in the same location as your &lt;code&gt;docker-compose.yml&lt;/code&gt; file and add a &lt;code&gt;Caddyfile&lt;/code&gt; inside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yourdomain.com &lt;span class="o"&gt;{&lt;/span&gt;
  reverse_proxy web:8000
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will make sure Caddy forwards all requests from your domain to the Django app running on port 8000. It's important to provide your actual domain name here, as Caddy uses it to obtain SSL certificates automatically. Use A/AAAA records to point your domain to your server's IP address so requests reach your server.&lt;/p&gt;

&lt;p&gt;Once your domain is setup and points to your server, you can spin up the services. On the server, authenticate with your container registry first (see previous section), then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Your Django app should now be running on your VPS.&lt;/p&gt;

&lt;p&gt;To deploy a new version of your app, pull the new image and restart the web service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose pull web
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As mentioned before, you can automate deployments with a CI/CD pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Deploying Django on a VPS is cheap and a great way to learn about infrasturcture. &lt;/p&gt;

&lt;p&gt;What we used in this guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker to containerize the Django app&lt;/li&gt;
&lt;li&gt;Postgres as the database&lt;/li&gt;
&lt;li&gt;Caddy as a reverse proxy for SSL termination&lt;/li&gt;
&lt;li&gt;Hetzner as the VPS provider&lt;/li&gt;
&lt;li&gt;GitHub Container Registry to store the Docker image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are endless ways to improve this setup: add backups, monitoring, logging, scaling, and more. When you start your self-hosting journey, prepare to learn a lot! If you want to save yourself some hassle, check out our product: &lt;a href="https://sliplane.io?utm_source=django-vps" rel="noopener noreferrer"&gt;sliplane.io&lt;/a&gt;, which comes with all of this out of the box. It's a middle ground that gives you the best of self-hosting  without the pain of ongoing server maintenance.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>webdev</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>5 easy ways to deploy Docker containers</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Wed, 05 Nov 2025 10:39:15 +0000</pubDate>
      <link>https://dev.to/wimadev/5-easy-ways-to-deploy-docker-containers-4dgg</link>
      <guid>https://dev.to/wimadev/5-easy-ways-to-deploy-docker-containers-4dgg</guid>
      <description>&lt;p&gt;It's easy to spin up containers locally. However, if you want to deploy them it can be a bit intimidating. But the hosting landscape drastically changed in recent years — now there are plenty of options available that make hosting containers simple. Let's look at 5 different options ranked by complexity, starting with the easiest way to deploy your containers!&lt;/p&gt;

&lt;h2&gt;
  
  
  At a glance - ordered by simplicity:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sliplane.io?utm_source=5-easy-ways-to-deploy-containers" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; provides the easiest solution for deploying containerized applications at a reasonable price. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://render.com" rel="noopener noreferrer"&gt;Render&lt;/a&gt; offers advanced scaling features and is also simple and straightforward to use, but it comes at a premium price point. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://digitalocean.com" rel="noopener noreferrer"&gt;Digital Ocean&lt;/a&gt;'s App Platform can be seen as a middleground between Sliplane and Render in terms of simplicity, pricing and scalability. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://coolify.io" rel="noopener noreferrer"&gt;Coolify&lt;/a&gt; simplifies hosting containers on virtual private servers. It's open source and therefore a cheap option but on the other hand requires some ongoing maintenance effort.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hetzner.cloud/?ref=mZziDsGU2VVp" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt; provides plain virtual private servers - it offers the most flexibility, but also requires the most setup and maintenance effort &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. &lt;a href="https://sliplane.io?utm_source=5-easy-ways-to-deploy-containers" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F40hxljdgeuy22bftnpw1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F40hxljdgeuy22bftnpw1.webp" alt=" " width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sliplane is arguably one of the easiest ways to deploy containerized applications. It's a European hosting platform focused on one thing and one thing only: containers.&lt;/p&gt;

&lt;p&gt;You can quickly spin up containers from a registry like Docker Hub or GitHub Container Registry or build and deploy your own code directly from a GitHub repository.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;attach persistent volumes&lt;/li&gt;
&lt;li&gt;automatic daily volume backups&lt;/li&gt;
&lt;li&gt;automatic build and deployment pipelines&lt;/li&gt;
&lt;li&gt;secrets management&lt;/li&gt;
&lt;li&gt;custom domains&lt;/li&gt;
&lt;li&gt;SSL&lt;/li&gt;
&lt;li&gt;and much more...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the key selling points is the fact that you don't pay extra for every service that you deploy. Instead, you pay per server and you can deploy as much as your server can handle.&lt;/p&gt;

&lt;p&gt;If you are looking for a simple and cost-effective way to deploy your containers with minimal overhead, Sliplane is the way to go. &lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;a href="https://render.com" rel="noopener noreferrer"&gt;Render&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2f6x133hz5bzpmcf737.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2f6x133hz5bzpmcf737.webp" alt=" " width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at simplicity, Render is a great option as well — it comes with an intuitive UI to make deploying containers buttery smooth.&lt;/p&gt;

&lt;p&gt;Additionally, Render offers some advanced scaling features that allow you to handle huge traffic and also offers additional features like fully managed databases, for example.&lt;/p&gt;

&lt;p&gt;The main difference in comparison to Sliplane: the price point.&lt;/p&gt;

&lt;p&gt;While Render is also very simple to use and abstracts many of the infrastructure-level tasks, it comes at a premium price point. Depending on your configuration, compute, storage, and bandwidth costs can be up to 10x higher than Sliplane's, and you will be charged extra for collaborators.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;a href="https://digitalocean.com" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1dtj8r5978sgluqatq8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1dtj8r5978sgluqatq8.webp" alt=" " width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DigitalOcean is a classic. They provide a wide range of services from bare metal to managed Kubernetes to their fully managed App Platform that you can use to deploy containers.&lt;/p&gt;

&lt;p&gt;Looking at simplicity, App Platform is their easiest product to get started. It's a higher-level abstraction on top of Kubernetes and can be categorized as the middle ground between Sliplane and Render.&lt;/p&gt;

&lt;p&gt;In comparison to Render, DigitalOcean's App Platform is cheaper but lacks some convenience features here and there, although the differences are nuanced.&lt;/p&gt;

&lt;p&gt;Compared to Sliplane, DigitalOcean's App Platform is more expensive but has more advanced scaling features. A key differentiator is Sliplane's focus on containers, which makes the transition from local development to deploying the container more seamless, while you might have to spend a thought more about how to wrap your app in DigitalOcean concepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;a href="https://coolify.io" rel="noopener noreferrer"&gt;Coolify&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjpbm8bt5utei6cp2xjme.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjpbm8bt5utei6cp2xjme.webp" alt=" " width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Coolify is an open-source PaaS. As stated on the website, it's a self-hostable alternative to popular commercial PaaS providers like the three mentioned above. In addition to the self-hosted version, they also provide a managed plan for Coolify.&lt;/p&gt;

&lt;p&gt;The big difference: you bring your own servers. This means you can rent virtual private servers from a cloud provider of choice and Coolify handles the deployments.&lt;/p&gt;

&lt;p&gt;The big benefit: it's cheap! You can rent a powerful VPS from $5 per month and only pay for the compute resources. In addition to that, you get full control over your servers and you can reduce third-party dependencies.&lt;/p&gt;

&lt;p&gt;The downside: you still need to manage these servers — keep the operating system up to date, install the latest patches and make sure to keep them secure. While Coolify is a big improvement in comparison to a plain VPS, the setup and maintenance are still more complicated compared to a fully managed PaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;a href="https://hetzner.cloud/?ref=mZziDsGU2VVp" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbu4nnz03moasn53241nu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbu4nnz03moasn53241nu.webp" alt=" " width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hetzner is a cloud provider that is known for cheap and reliable infrastructure. They provide virtual private servers with a great price-to-performance ratio.&lt;/p&gt;

&lt;p&gt;When it comes to deploying Docker containers, using a plain VPS is still a great choice.&lt;/p&gt;

&lt;p&gt;To be fair, it's not the easiest way to deploy a container, but it gives you the flexibility that you need to deploy anything.&lt;/p&gt;

&lt;p&gt;Compute prices are unbeatable, but you're trading time for money.&lt;/p&gt;

&lt;p&gt;You can use our affiliate link to get a 20€ discount.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Sliplane is aguably the easiest way to get your containers running in the cloud for an affordable price. Render and Digital Ocean are also great managed solutions, they are more scalable but more expensive and don't have the same focus on containers as Sliplane. Coolify is an honorable mention, as an affordable open source option. If you want full control, I recommend Hetzner as a VPS provider.&lt;/p&gt;

&lt;p&gt;Disclaimer: The options listed above have been carefully handpicked by a human and are carefully chosen. I do get a kickback from Hetzner if you signup through the link and as a co-founder, I'm biased with Sliplane.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>webdev</category>
      <category>programming</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Does it Make Sense to Run WordPress in Docker?</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Thu, 07 Aug 2025 10:30:33 +0000</pubDate>
      <link>https://dev.to/wimadev/does-it-make-sense-to-run-wordpress-in-docker-4655</link>
      <guid>https://dev.to/wimadev/does-it-make-sense-to-run-wordpress-in-docker-4655</guid>
      <description>&lt;p&gt;I've had my ups and downs with &lt;a href="https://wordpress.org" rel="noopener noreferrer"&gt;WordPress&lt;/a&gt;, I'm not a hardcore fan to be honest, but you can't deny it's popularity. &lt;/p&gt;

&lt;p&gt;You can use &lt;a href="https://docker.com" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; to spin up an instance of WordPress on your local computer and in the cloud. But does it make sense to use WordPress in Docker?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it doesn't make sense to run Wordpress in Docker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For non technical users, running Docker locally is usually too much overhead. It comes with a learning curve and there are other tools that make getting started much easier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local development tools&lt;/strong&gt; like &lt;a href="https://www.apachefriends.org/" rel="noopener noreferrer"&gt;XAMPP&lt;/a&gt;, &lt;a href="https://www.mamp.info/" rel="noopener noreferrer"&gt;MAMP&lt;/a&gt;, or &lt;a href="https://localwp.com/" rel="noopener noreferrer"&gt;Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed hosting&lt;/strong&gt; - ditch the local setup and go straight to a managed WordPress platform like &lt;a href="https://wordpress.com/" rel="noopener noreferrer"&gt;WordPress.com&lt;/a&gt;, &lt;a href="https://wpengine.com/" rel="noopener noreferrer"&gt;WP Engine&lt;/a&gt;, or &lt;a href="https://kinsta.com/" rel="noopener noreferrer"&gt;Kinsta&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traditional shared hosting&lt;/strong&gt;: still the cheapest option for basic WordPress sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can still use Docker of course, there is nothing inherently wrong with a WordPress in Docker setup. In fact, there are some good reasons to...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it makes sense to run WordPress in Docker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are a developer on the other hand and maybe even work in a team with frequent WordPress deployments, running WordPress in Docker makes a lot of sense. It is really useful to mirror the production environment on your own machine for development and easily share it with the team so all developers have consistent environments, especially if you work on multiple sites with different PHP versions, databases, or OS plugins.&lt;/p&gt;

&lt;p&gt;In combination with &lt;a href="https://sliplane.io?utm_source=wordpress-in-docker" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt;, you can easily deploy your containerized apps and share progress internally and with clients, integrate the deployment into a QA pipeline or run it in production.&lt;/p&gt;

&lt;p&gt;I know a lot of developers who don't want to deal with Docker at all and I used to be that guy as well. In the beginning it can feel like a lot of painful overhead to get the setup going. However, there will be a turning point, after you got your head wrapped around some basic Docker concepts and at that point you don't want to go back!&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you run WordPress in Docker in production?
&lt;/h2&gt;

&lt;p&gt;Absolutely. Running WordPress in Docker is possible in a production setting as well. &lt;/p&gt;

&lt;p&gt;If you are familiar with Docker, it's fairly easy to spin up small to medium sized WordPress-in-Docker setups, the harder thing is keeping it secure and performing consistent maintenance. If you don't want to deal with that it's probably a good idea to go with a &lt;a href="https://sliplane.io?utm_source=wordpress-in-docker" rel="noopener noreferrer"&gt;managed Docker hosting solution&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Some benefits of running WordPress in Docker: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;very flexible, you can add and install anything&lt;/li&gt;
&lt;li&gt;affordable: you get big compute for little money&lt;/li&gt;
&lt;li&gt;portable, you can easily move to a different host&lt;/li&gt;
&lt;li&gt;it's widely adopted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running WordPress in Docker can get challenging, once you reach a certain size with tens of thousands of pages, millions of monthly active users and hundreds of Gigabytes of file storage. But even then, with a few tweaks the setup can scale very large. Here are some things to think about, although I usually recommend to only add these components, once you really need them (and you can feel that: slow site, storage full, crashes, ...)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Persistent storage, it's okay to store themes on your server, however when it comes to media, you're better of with choosing an object storage provider. There are plugins like &lt;a href="https://wordpress.org/plugins/ilab-media-tools/" rel="noopener noreferrer"&gt;Media Cloud&lt;/a&gt; that make the switch very easy.&lt;/li&gt;
&lt;li&gt;Caching, at a certain scale it makes sense to add caching to speed up requests, for example use &lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; via the &lt;a href="https://wordpress.org/plugins/redis-cache/" rel="noopener noreferrer"&gt;Redis Cache plugin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Separate Database: move the database to a separate machine. It makes sense to keep the database in the same network though and use private connections if possible.&lt;/li&gt;
&lt;li&gt;Load Balancing: If vertical scaling does not do the trick any more, you can add a load balancer in front of your WordPress instance and spread the load to multiple instances. This requires your WordPress containers to be stateless though&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Run WordPress in Docker
&lt;/h2&gt;

&lt;p&gt;We need to setup &lt;strong&gt;two containers&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Database container&lt;/strong&gt; (MySQL/MariaDB) - stores your WordPress content, users, settings, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress container&lt;/strong&gt; - runs the PHP application and serves your website&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Using Docker CLI
&lt;/h3&gt;

&lt;p&gt;Let's start by creating a network, so the containers can communicate with each other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a network&lt;/span&gt;
docker network create wordpress-network
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, spin up the database container, we'll use MySQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run MySQL&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; wordpress-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; wordpress-network &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wordpress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wp_user &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wp_pass &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;root_password &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; mysql_data:/var/lib/mysql &lt;span class="se"&gt;\&lt;/span&gt;
  mysql:9.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Breaking down this command:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker run -d&lt;/code&gt; - Runs the container in detached mode (in the background)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--name wordpress-db&lt;/code&gt; - Gives the container a friendly name we can reference&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--network wordpress-network&lt;/code&gt; - Connects the container to our custom network&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-e MYSQL_DATABASE=wordpress&lt;/code&gt; - Creates a database called "wordpress" &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-e MYSQL_USER=wp_user&lt;/code&gt; - Creates a MySQL user for WordPress to use&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-e MYSQL_PASSWORD=wp_pass&lt;/code&gt; - Sets the password for that user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-e MYSQL_ROOT_PASSWORD=root_password&lt;/code&gt; - Sets the MySQL root password&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-v mysql_data:/var/lib/mysql&lt;/code&gt; - Creates a persistent volume to store database data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mysql:9.4&lt;/code&gt; - Uses the official &lt;a href="https://hub.docker.com/_/mysql" rel="noopener noreferrer"&gt;MySQL 9.4 Docker image&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now that our database is running, we can deploy the WordPress container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run WordPress&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; wordpress-site &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; wordpress-network &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;WORDPRESS_DB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wordpress-db:3306 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;WORDPRESS_DB_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wordpress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;WORDPRESS_DB_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wp_user &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;WORDPRESS_DB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wp_pass &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; wordpress_data:/var/www/html &lt;span class="se"&gt;\&lt;/span&gt;
  wordpress:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Breaking down this command:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker run -d&lt;/code&gt; - Runs in detached mode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--name wordpress-site&lt;/code&gt; - Names the container for easy reference&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--network wordpress-network&lt;/code&gt; - Connects to the same network as our database&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-p 8080:80&lt;/code&gt; - Maps port 8080 on your computer to port 80 in the container&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-e WORDPRESS_DB_HOST=wordpress-db:3306&lt;/code&gt; - Tells WordPress where to find the database (using the container name)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-e WORDPRESS_DB_NAME=wordpress&lt;/code&gt; - Specifies which database to use&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-e WORDPRESS_DB_USER=wp_user&lt;/code&gt; - Database username (must match what we set in MySQL)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-e WORDPRESS_DB_PASSWORD=wp_pass&lt;/code&gt; - Database password (must match what we set in MySQL)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-v wordpress_data:/var/www/html&lt;/code&gt; - Persists WordPress files and uploads&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wordpress:latest&lt;/code&gt; - Uses the official &lt;a href="https://hub.docker.com/_/wordpress" rel="noopener noreferrer"&gt;WordPress Docker image&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your WordPress site will be available at &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Docker Compose
&lt;/h3&gt;

&lt;p&gt;You can also use &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; to simplify the process:&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wordpress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wordpress:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;WORDPRESS_DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db:3306&lt;/span&gt;
      &lt;span class="na"&gt;WORDPRESS_DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wordpress&lt;/span&gt;
      &lt;span class="na"&gt;WORDPRESS_DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wp_user&lt;/span&gt;
      &lt;span class="na"&gt;WORDPRESS_DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wp_pass&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;wordpress_data:/var/www/html&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:9.4&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wordpress&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wp_user&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wp_pass&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root_password&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/mysql&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wordpress_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy WordPress in Docker
&lt;/h2&gt;

&lt;p&gt;There are a few providers that you can deploy your containers to. If you are looking for a solution with minimum overhead, I recommend &lt;a href="https://sliplane.io?utm_source=wordpress-in-docker" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a New Project
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://sliplane.io" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; and sign in with your GitHub account.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create Project"&lt;/strong&gt; and give your project a name (e.g., &lt;code&gt;WordPress&lt;/code&gt;). This project will serve as the container environment where you’ll deploy two services: one for MySQL and one for WordPress.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Deploy a MySQL Service
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Inside the project dashboard, click "Deploy Service."&lt;/li&gt;
&lt;li&gt;Create a new server to host your MySQL service. You can select the location and server type, the base server should be strong enough to get started.&lt;/li&gt;
&lt;li&gt;Choose the "MySQL" preset from the available services. This preset uses the &lt;code&gt;bitnami/mysql&lt;/code&gt; image under the hood and comes with sensible default configurations. You can also use other database containers like MariaDB, which work similarly with WordPress.&lt;/li&gt;
&lt;li&gt;Review Configuration: The MySQL preset comes preconfigured and can be deployed as is, but you can customize it further if needed. I recommend turning off public access to the database for security reasons.&lt;/li&gt;
&lt;li&gt;Click "Deploy Service" and wait a few seconds for the deploy to finish.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Deploy a WordPress Service
&lt;/h3&gt;

&lt;p&gt;After your MySQL service is successfully deployed and running, the next step is to deploy the WordPress CMS container. This container will connect to your MySQL database to store and retrieve data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inside your "WordPress" project, click "Deploy Service" again.&lt;/li&gt;
&lt;li&gt;Select the server that your MySQL service is running on.&lt;/li&gt;
&lt;li&gt;Select "Registry" as the deploy source&lt;/li&gt;
&lt;li&gt;In the "Image URL" field, type "wordpress" and choose the official WordPress image from the dropdown. We go with latest version, but you can choose a specific tag if wantet.&lt;/li&gt;
&lt;li&gt;In the Environment Variables section, configure the following:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   WORDPRESS_DB_HOST=internal-mysql-hostname
   WORDPRESS_DB_NAME=wordpress
   WORDPRESS_DB_USER=wp_user
   WORDPRESS_DB_PASSWORD=wp_pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Make sure these values match exactly with what you configured in your MySQL service. The &lt;code&gt;WORDPRESS_DB_HOST&lt;/code&gt; should use the internal host name of your MySQL container.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol start="6"&gt;
&lt;li&gt;Add a new volume with a name of your choice and set the mount path to "/var/www/html". This is where your WordPress files and uploads will be stored.&lt;/li&gt;

&lt;li&gt;Finally, give the service a name and hit "Deploy Service".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After the deploy is finished, you can access your WordPress site at the URL that has been assigned and continue with the installation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;For non-technical users, traditional hosting is easier, however, for developers who want to maintain consistent environments and work as close to production as possible, using WordPress in Docker is a great choice.&lt;/p&gt;

&lt;p&gt;You can also run WordPress-Docker setups in production, but it requires some technical knowledge and if you don't want to deal with deal with the overhead, a managed solution like &lt;a href="https://sliplane.io?utm_source=wordpress-in-docker" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; is the way to go.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>programming</category>
      <category>webdev</category>
      <category>docker</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Fri, 25 Jul 2025 05:29:49 +0000</pubDate>
      <link>https://dev.to/wimadev/-1bec</link>
      <guid>https://dev.to/wimadev/-1bec</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/wimadev" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1134748%2F7788db9e-91c1-44a6-93d1-28d6bec76793.jpeg" alt="wimadev"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/wimadev/how-to-run-payload-cms-in-docker-5gn1" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;How to Run Payload CMS in Docker&lt;/h2&gt;
      &lt;h3&gt;Lukas Mauser ・ Jul 21&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#docker&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#programming&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>webdev</category>
      <category>docker</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Best FREE Secrets Manager - Deploy Infisical on Sliplane with Docker</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Thu, 24 Jul 2025 07:37:56 +0000</pubDate>
      <link>https://dev.to/wimadev/best-free-secrets-manager-deploy-infisical-on-sliplane-with-docker-2dem</link>
      <guid>https://dev.to/wimadev/best-free-secrets-manager-deploy-infisical-on-sliplane-with-docker-2dem</guid>
      <description>&lt;p&gt;&lt;a href="https://infisical.com/" rel="noopener noreferrer"&gt;Infisical&lt;/a&gt; is an open-source secrets management platform that helps you securely store, sync, and manage your application secrets across your entire development lifecycle. It provides a secure vault for API keys, database credentials, certificates, and other sensitive data with features like secret versioning, audit logs, and integrations with popular development tools.&lt;/p&gt;

&lt;p&gt;We've been using this secrets manager at our company for a while and all I can say is: I am impressed! The product is rock solid and it's super simple to setup your own instance.&lt;/p&gt;

&lt;p&gt;In this guide, I'll show you how to deploy your own Infisical instance in the cloud using &lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; and &lt;a href="https://sliplane.io?utm_source=infisical" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Our Infisical deployment will consist of three services:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; - Main database for storing secrets and metadata&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; - Caching layer for improved performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infisical&lt;/strong&gt; - The main application server&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Deploy in the Cloud
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Create a New Project
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Log in to &lt;a href="https://sliplane.io?utm_source=infisical" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; with your GitHub account&lt;/li&gt;
&lt;li&gt;In the Dashboard, click "Create Project" and name it "infisical"&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Deploy PostgreSQL Database
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to your new project and click "Deploy Service"&lt;/li&gt;
&lt;li&gt;Select a server or create a new one if you don't have one yet. To create a new server, click "Create Server", then choose the location and server type. The base server type should be enough to get started - you can scale up later if needed&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Postgres&lt;/strong&gt; from the presets&lt;/li&gt;
&lt;li&gt;In the settings:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disable the public toggle&lt;/strong&gt; for additional security&lt;/li&gt;
&lt;li&gt;You can change the default database name, user, and password if desired, you'll need these credentials later for deploying Infisical&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click "Deploy" and wait a few seconds for your database to deploy&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Deploy Redis
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the same project, click "Deploy Service" again&lt;/li&gt;
&lt;li&gt;Select the same server where PostgreSQL is running&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Redis&lt;/strong&gt; from the presets&lt;/li&gt;
&lt;li&gt;In the settings:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disable the public toggle&lt;/strong&gt; for additional security&lt;/li&gt;
&lt;li&gt;Like in PostgreSQL, you can change the default password if desired, which you will need later for deploying Infisical&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click "Deploy" and wait a few seconds for Redis to come live&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 4: Deploy Infisical
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the infisical project, click "Deploy Service" again&lt;/li&gt;
&lt;li&gt;Select the same server where PostgreSQL and Redis are running&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Registry&lt;/strong&gt; as the deploy source&lt;/li&gt;
&lt;li&gt;In the "Image URL" field, enter: &lt;code&gt;docker.io/infisical/infisical:v0.137.0-postgres&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add the following environment variables, but make sure to replace the placeholders with your actual Postgres and Redis connection details!
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AUTH_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"q6LRi7c717a3DQ8JUxlWYkZpMhG4+RHLoFUVt3Bvo2U="&lt;/span&gt;
&lt;span class="nv"&gt;DB_CONNECTION_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pg://postgres:s2H8ivfQidmNzfA4@postgres-wxzi.internal:5432/infiscal"&lt;/span&gt;
&lt;span class="nv"&gt;ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"f40c9178624764ad85a6830b37ce239a"&lt;/span&gt;
&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0"&lt;/span&gt;
&lt;span class="nv"&gt;REDIS_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"redis://:qclE92PDoGjNg3rP@redis-t9x2.internal:6379"&lt;/span&gt;
&lt;span class="nv"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SLIPLANE_DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: You need to update the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;s2H8ivfQidmNzfA4&lt;/code&gt; with your PostgreSQL password&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;postgres-wxzi.internal&lt;/code&gt; with your PostgreSQL internal hostname&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;infiscal&lt;/code&gt; with your database name (if you changed it)&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;qclE92PDoGjNg3rP&lt;/code&gt; with your Redis password&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;redis-t9x2.internal&lt;/code&gt; with your Redis internal hostname&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To find these values:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to your PostgreSQL service in a new tab - you'll see the internal hostname and connection details in the environment variables section&lt;/li&gt;
&lt;li&gt;Navigate to your Redis service in another tab - you'll see the internal hostname and password in the environment variables section&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Click "Deploy" and wait for the deployment to complete. Once deployed, you can access Infisical at your &lt;code&gt;...sliplane.app&lt;/code&gt; domain&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpa31fvk52v624t7rp0ol.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpa31fvk52v624t7rp0ol.webp" alt="Infisical UI" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Infisical provides a flexible, open-source alternative to commercial secrets management platforms like HashiCorp Vault or AWS Secrets Manager. Self-hosting gives you complete control over your sensitive data and the freedom to customize as needed.&lt;/p&gt;

&lt;p&gt;This straightforward three-service setup with PostgreSQL and Redis containerized approach makes it simple to replicate across different environments or adapt to your specific requirements.&lt;/p&gt;

&lt;p&gt;You now have a functional secrets management platform that you can easily extend or integrate with your existing tools. For deployment, we used &lt;a href="https://sliplane.io?utm_source=infisical" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; which simplified the Docker orchestration and inter-service networking.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>docker</category>
      <category>sliplane</category>
      <category>devops</category>
    </item>
    <item>
      <title>A FREE and Open Source Airtable Alternative - How to Spin Up NocoDB Using Docker</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Wed, 23 Jul 2025 08:00:52 +0000</pubDate>
      <link>https://dev.to/wimadev/a-free-and-open-source-airtable-alternative-how-to-spin-up-nocodb-using-docker-510a</link>
      <guid>https://dev.to/wimadev/a-free-and-open-source-airtable-alternative-how-to-spin-up-nocodb-using-docker-510a</guid>
      <description>&lt;p&gt;&lt;a href="https://nocodb.com/" rel="noopener noreferrer"&gt;NocoDB&lt;/a&gt; is an open-source Airtable alternative. On their site they claim that it "allows building no-code database solutions with ease of spreadsheets." You can turn any database into a smart spreadsheet interface, create forms, build APIs, and collaborate with your team.&lt;/p&gt;

&lt;p&gt;In this guide, I want to show you how easy it is to spin up your own instance of this free database management tool in the cloud using &lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; and &lt;a href="https://sliplane.io?utm_source=nocodb" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local Testing&lt;/li&gt;
&lt;li&gt;Run in the Cloud&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Local testing
&lt;/h2&gt;

&lt;p&gt;You can test the setup locally on your computer. Just make sure &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt; is installed and running on your machine. However its even easier if you do it directly in Sliplane, what I will describe in the next section.&lt;/p&gt;

&lt;h3&gt;
  
  
  With SQLite
&lt;/h3&gt;

&lt;p&gt;Open a terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; nocodb &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/nocodb:/usr/app/data/ &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
    nocodb/nocodb:0.263.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We use docker run to spin up a new container&lt;/li&gt;
&lt;li&gt;We name it nocodb, you can choose a different name&lt;/li&gt;
&lt;li&gt;We mount a new volume to &lt;code&gt;/usr/app/data&lt;/code&gt;, this means everything nocodb saves in this directory will be stored outside the container and persist even if the container gets shut down or destroyed at some point. Nocodb will create our SQLite database file in here&lt;/li&gt;
&lt;li&gt;We map port 8080 inside the container to 8080 on our host&lt;/li&gt;
&lt;li&gt;We specify the image to run: nocodb/nocodb along with the version of the image (here: &lt;code&gt;0.263.8&lt;/code&gt;, but you can use a different version if needed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can test if the app is running by navigating to &lt;code&gt;http://localhost:8080&lt;/code&gt; in your web browser. Documentation for how you can configure NocoDB can be found &lt;a href="https://hub.docker.com/r/nocodb/nocodb" rel="noopener noreferrer"&gt;on Dockerhub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7bwd4nxpuzz2svofrcv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7bwd4nxpuzz2svofrcv.webp" alt="NocoDB Login Screen" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Login Screen after first install.&lt;/p&gt;

&lt;h3&gt;
  
  
  With Postgres
&lt;/h3&gt;

&lt;p&gt;If you want to use postgres as the underlying database, you need to run a postgres container first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new network
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create nocodb-net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a postgres container with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--name&lt;/span&gt; nocodb-postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysecretpassword &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nocodb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/postgres:/var/lib/postgresql/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; nocodb-net &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; postgres:17.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We use docker run to spin up a new container&lt;/li&gt;
&lt;li&gt;We name it nocodb-postgres, you can choose a different name&lt;/li&gt;
&lt;li&gt;We set a database and password with the &lt;code&gt;-e&lt;/code&gt; envs&lt;/li&gt;
&lt;li&gt;We mount a new volume to /var/lib/postgresql/data, this means everything postgres saves in this directory will be stored outside the container and persist even if the container gets shut down or destroyed at some point. Note: Postgres changed this mount path after version 18 - please always refer to the docs, depending on what version you are using&lt;/li&gt;
&lt;li&gt;We specify the image to run: postgres along with the version of the image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, I used &lt;code&gt;17.2&lt;/code&gt; as the version. Make sure to check the &lt;a href="https://hub.docker.com/_/postgres" rel="noopener noreferrer"&gt;postgres documentation on Docker Hub&lt;/a&gt; to find configurations that matches the pg version you are using.&lt;/p&gt;

&lt;p&gt;Next, start the nocodb container and connect it to the postgres database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; nocodb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/nocodb:/usr/app/data/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; nocodb-net &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;NC_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pg://nocodb-postgres:5432?u=postgres&amp;amp;p=mysecretpassword&amp;amp;d=nocodb"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;NC_AUTH_JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-jwt-auth-secret"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  nocodb/nocodb:0.263.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We use docker run to spin up a new container&lt;/li&gt;
&lt;li&gt;We name it &lt;code&gt;nocodb&lt;/code&gt;, you can choose a different name&lt;/li&gt;
&lt;li&gt;We mount a volume &lt;code&gt;-v&lt;/code&gt; to &lt;code&gt;/usr/app/data/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;We set a JWT auth secret and the database URI in order to connect to our postgres database using the postgres user and password&lt;/li&gt;
&lt;li&gt;We specify the image to run: nocodb/nocodb along with the version of the image (&lt;code&gt;0.263.8&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can now access nocodb on &lt;code&gt;http://localhost:8080&lt;/code&gt; on your own host.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using docker compose
&lt;/h3&gt;

&lt;p&gt;You can also use Docker Compose instead. Create a file called &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17.2&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nocodb-postgres&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nocodb&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysecretpassword&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nocodb-net&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;nocodb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nocodb/nocodb:0.263.8&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nocodb&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NC_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg://postgres:5432?u=postgres&amp;amp;p=mysecretpassword&amp;amp;d=nocodb"&lt;/span&gt;
      &lt;span class="na"&gt;NC_AUTH_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-jwt-auth-secret"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nocodb_data:/usr/app/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nocodb-net&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nocodb_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nocodb-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run in the cloud:
&lt;/h2&gt;

&lt;p&gt;We use Sliplane to run it in the cloud. Sliplane makes running containers very easy and affordable in the cloud. &lt;/p&gt;

&lt;p&gt;In general, we just follow the steps like in our local setup, but with a few simple changes, in fact, it is even easier on Sliplane.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Deploy a Postgres Database
&lt;/h3&gt;

&lt;p&gt;Note: If you want to use SQL Lite you can skip this part and continue with step 2&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to &lt;a href="https://sliplane.io?utm_source=nocodb" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; with your GitHub account. In the Dashboard, click "Create Project" and name it "nocodb".&lt;/li&gt;
&lt;li&gt;Click "Deploy Service".&lt;/li&gt;
&lt;li&gt;Select a server or create a new one if you don't have one yet. To create a new one, click "Create Server", then choose the location and server type. The base server type should be enough to get started, you can scale up later if you need to.&lt;/li&gt;
&lt;li&gt;Choose Postgres from the presets. In the settings, disable the public toggle for additional security. You can also change the default database name, user, and password if you want. Then click deploy and wait for your database to deploy.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2. Deploy Nocodb
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the project we created in step 1, click "Deploy Service" again.&lt;/li&gt;
&lt;li&gt;Select the server where Postgres is running and in the "Deploy From" section select "Registry".&lt;/li&gt;
&lt;li&gt;In the "Image URL" field, search for nocodb and choose the &lt;code&gt;nocodb/nocodb&lt;/code&gt; image and a tag from the list.&lt;/li&gt;
&lt;li&gt;Add the following environment variables, but make sure to change the password, database, and internal host to match the settings from your Postgres database. I recommend you open a new tab with the Postgres service on Sliplane to grab these values from:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NC_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pg://[postgres.internal]:5432?u=postgres&amp;amp;p=[password]&amp;amp;d=[database]"&lt;/span&gt;
&lt;span class="nv"&gt;NC_AUTH_JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-secure-jwt-secret"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Add a volume to your nocodb service, give it a name of your choice and set the mount path to &lt;code&gt;/usr/app/data&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click "Deploy" and wait for the deploy to finish.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can now access nocodb through your &lt;code&gt;...sliplane.app&lt;/code&gt; domain that is showing in the dashboard!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqaodm85xduogrqqmqgw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqaodm85xduogrqqmqgw.webp" alt="NocoDB UI" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;NocoDB is a solid free alternative to Airtable (although Airtable now has more product offerings beyond turining databases into spreadsheets). By self-hosting it, you get your own data and control, and pretty much the same functionality at a fraction of the cost compared to commercial SaaS options.&lt;/p&gt;

&lt;p&gt;Setting it up with Docker on &lt;a href="https://sliplane.io?utm_source=nocodb" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; is possible within minutes.&lt;/p&gt;

</description>
      <category>watercooler</category>
      <category>docker</category>
      <category>opensource</category>
      <category>database</category>
    </item>
    <item>
      <title>How to Run Payload CMS in Docker</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Mon, 21 Jul 2025 21:01:39 +0000</pubDate>
      <link>https://dev.to/wimadev/how-to-run-payload-cms-in-docker-5gn1</link>
      <guid>https://dev.to/wimadev/how-to-run-payload-cms-in-docker-5gn1</guid>
      <description>&lt;p&gt;&lt;a href="https://payloadcms.com/" rel="noopener noreferrer"&gt;Payload&lt;/a&gt; is an open source backend framework and it is mainly used as a content management system.&lt;/p&gt;

&lt;p&gt;You can use Docker to run your own instance of Payload on &lt;a href="https://sliplane.io?utm_source=payload" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt;, however, when I tried using the Dockerfile that gets created using the &lt;code&gt;pnpx create-payload-app&lt;/code&gt; it did not work for me right away and I had to apply a few tweaks and settings in order to get payload running. &lt;/p&gt;

&lt;p&gt;Here's how you can run Payload with Docker:&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;NodeJs and Docker should be installed on your system. For the demo I use pnpm as a package manager so make sure to install it as well or tweak the installation instructions to use your package manager of choice.&lt;/p&gt;

&lt;p&gt;In my case I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node version: v22.12.0&lt;/li&gt;
&lt;li&gt;pnpm version: 9.13.2
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create a new Payload App
&lt;/h2&gt;

&lt;p&gt;Open a new terminal in the parent folder where your Payload project should be created. Run the install wizard with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# npx&lt;/span&gt;
npx create-payload-app

&lt;span class="c"&gt;# or pnpx&lt;/span&gt;
pnpx create-payload-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will be guided through a series of short questions, I'll use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Payload Demo" as my project name&lt;/li&gt;
&lt;li&gt;setup a blank project&lt;/li&gt;
&lt;li&gt;go with MongoDB as my database and use the default connection setting to begin with&lt;/li&gt;
&lt;li&gt;and pnpm as the package manager
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To install payload into an existing project, please follow the installation instructions on the official &lt;a href="https://payloadcms.com/docs/getting-started/installation" rel="noopener noreferrer"&gt;payload documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After the wizard has finished, we can see that the project includes a Dockerfile out of the box. However, when I tried to use this Dockerfile, I ran into several issues that prevented it from working properly in my environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues I Encountered
&lt;/h2&gt;

&lt;p&gt;When trying to run the generated Dockerfile, I encountered a few problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unpinned pnpm version&lt;/strong&gt; - The Dockerfile didn't specify a specific pnpm version, which caused my Docker builds to fail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing standalone mode&lt;/strong&gt; - My builds also failed because Next.js wasn't configured for standalone output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public folder issues&lt;/strong&gt; - The Dockerfile tried to copy a public folder that didn't exist in my setup in the beginning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database connection issues&lt;/strong&gt; - I missed the &lt;code&gt;authSource&lt;/code&gt; connection parameter in the MongoDB connection URI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File upload permissions&lt;/strong&gt; - Media uploads failed due to incorrect folder permissions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let me show you how I fixed these issues one by one.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Fixed Dockerfile
&lt;/h2&gt;

&lt;p&gt;Here's the corrected Dockerfile that resolved the issues I encountered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.mjs file.&lt;/span&gt;
&lt;span class="c"&gt;# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:22.12.0-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies only when needed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;deps&lt;/span&gt;
&lt;span class="c"&gt;# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; libc6-compat
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies based on the preferred package manager&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; yarn.lock &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;yarn &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; package-lock.json &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;npm ci&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; pnpm-lock.yaml &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;corepack &lt;span class="nb"&gt;enable &lt;/span&gt;pnpm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; corepack prepare pnpm@9.13.2 &lt;span class="nt"&gt;--activate&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm i &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;else &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Lockfile not found."&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;fi&lt;/span&gt;


&lt;span class="c"&gt;# Rebuild the source code only when needed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=deps /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Next.js collects completely anonymous telemetry data about general usage.&lt;/span&gt;
&lt;span class="c"&gt;# Learn more here: https://nextjs.org/telemetry&lt;/span&gt;
&lt;span class="c"&gt;# Uncomment the following line in case you want to disable telemetry during the build.&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_TELEMETRY_DISABLED 1&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; yarn.lock &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;yarn run build&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; package-lock.json &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;npm run build&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; pnpm-lock.yaml &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;corepack &lt;span class="nb"&gt;enable &lt;/span&gt;pnpm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; corepack prepare pnpm@9.13.2 &lt;span class="nt"&gt;--activate&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm run build&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;else &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Lockfile not found."&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Production image, copy all the files and run next&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV production&lt;/span&gt;
&lt;span class="c"&gt;# Uncomment the following line in case you want to disable telemetry during runtime.&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_TELEMETRY_DISABLED 1&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; 1001 nodejs
&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; 1001 nextjs

&lt;span class="c"&gt;# Remove this line if you do not have this folder&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/public ./public&lt;/span&gt;

&lt;span class="c"&gt;# Set the correct permission for prerender cache&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; .next
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown &lt;/span&gt;nextjs:nodejs .next



&lt;span class="c"&gt;# Automatically leverage output traces to reduce image size&lt;/span&gt;
&lt;span class="c"&gt;# https://nextjs.org/docs/advanced-features/output-file-tracing&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=nextjs:nodejs /app/.next/standalone ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; media &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; nextjs:nodejs media

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nextjs&lt;/span&gt;


&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT 3000&lt;/span&gt;

&lt;span class="c"&gt;# server.js is created by next build from the standalone output&lt;/span&gt;
&lt;span class="c"&gt;# https://nextjs.org/docs/pages/api-reference/next-config-js/output&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; HOSTNAME="0.0.0.0" node server.js&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building and Running
&lt;/h2&gt;

&lt;p&gt;Here's how you can build and run your Payload CMS application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build the Docker image&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; payload-cms &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Run the container &lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DATABASE_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mongodb://your-mongo-host:27017/payload?authSource&lt;span class="o"&gt;=&lt;/span&gt;admin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;PAYLOAD_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-secret-key &lt;span class="se"&gt;\&lt;/span&gt;
  payload-cms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: If you want to persist uploaded files, you can mount a volume to &lt;code&gt;/app/media&lt;/code&gt;, however in a more production-ready setup, you might want to use an object storage solution&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pinning the pnpm Version
&lt;/h2&gt;

&lt;p&gt;To fix the pnpm version issue, I pinned the specific version in the Dockerfile in the deps and builder stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;corepack &lt;span class="nb"&gt;enable &lt;/span&gt;pnpm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; corepack prepare pnpm@9.13.2 &lt;span class="nt"&gt;--activate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the same pnpm version is used consistently across all environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Next.js for Standalone Output
&lt;/h2&gt;

&lt;p&gt;You'll also need to update your &lt;code&gt;next.config.mjs&lt;/code&gt; file to enable standalone output mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @type {import('next').NextConfig} */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;standalone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other configuration options&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Next.js to create a standalone version of your application that includes all the necessary dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the Public Folder
&lt;/h2&gt;

&lt;p&gt;If your project doesn't have a public folder yet, create one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;public
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; public/.gitkeep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps ensure the COPY command in the Dockerfile doesn't fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Configuration
&lt;/h2&gt;

&lt;p&gt;I used MongoDB in a container, and had to include the &lt;code&gt;authSource=admin&lt;/code&gt; parameter in the connection URI in order to connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URI=mongodb://username:password@localhost:27017/payload?authSource=admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  File Upload Permissions
&lt;/h2&gt;

&lt;p&gt;I attached the media folder to the Docker container using a volume mount. This allows you to persist uploaded files outside the container.&lt;/p&gt;

&lt;p&gt;For uploads to work correctly, I had to ensure the media folder had the right permissions, which I set in the Dockerfile with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; media &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; nextjs:nodejs media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates the media directory and sets the proper ownership so the nextjs user can write uploaded files to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Object Storage for Media Files
&lt;/h2&gt;

&lt;p&gt;At some point, you might want to configure object storage for media files instead of storing them in a volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// payload.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;s3Adapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@payloadcms/plugin-cloud-storage/s3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;buildConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;cloudStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;s3Adapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3_REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;accessKeyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;secretAccessKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="c1"&gt;// ... rest of your config&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy to Sliplane
&lt;/h2&gt;

&lt;p&gt;You can deploy your Payload CMS instance to &lt;a href="https://sliplane.io?utm_source=payload" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; by following a few simple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new project and give it a name of your choice&lt;/li&gt;
&lt;li&gt;Next we will deploy a MongoDB database&lt;/li&gt;
&lt;li&gt;Navigate into the project and click "Deploy Service" choose a server and select the MongoDB preset. You can make the service "private" since we don't want to expose it to the public internet&lt;/li&gt;
&lt;li&gt;Deploy the database, alternatively, you can tweak the connection settings like user, password and database name&lt;/li&gt;
&lt;li&gt;Next we will deploy Payload CMS and connect it to the MongoDB instance we just created&lt;/li&gt;
&lt;li&gt;In the project, click "Deploy Service" again, choose the same server, where your database is running on and select "Repository" as the deployment method&lt;/li&gt;
&lt;li&gt;In the repository URL field, look for your Payload CMS repository. If it does not show up in the list, make sure you granted Sliplane access to the repo using the "Configure Repository Access" button&lt;/li&gt;
&lt;li&gt;Add a volume, give it a name of your choice and mount it to &lt;code&gt;/app/media&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;PAYLOAD_SECRET&lt;/code&gt; and &lt;code&gt;DATABASE_URI&lt;/code&gt; environment variables. &lt;code&gt;PAYLOAD_SECRET&lt;/code&gt; is an arbitrary password. The database URI should look like this: &lt;code&gt;mongodb://username:password@mongodb:27017/payload?authSource=admin&lt;/code&gt; - You can find all connection settings like user, password, database and internal host name in the MongoDB service settings&lt;/li&gt;
&lt;li&gt;Click "Deploy" and wait for the deployment to finish&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Running Payload CMS in Docker requires a few configuration tweaks beyond the default setup. &lt;/p&gt;

&lt;p&gt;The key fixes include pinning the pnpm version, enabling Next.js standalone mode, setting proper file permissions, and optionally configuring object storage for production use.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>docker</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Guide: Deploy Ghost with Docker on Sliplane</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Sun, 13 Jul 2025 17:56:55 +0000</pubDate>
      <link>https://dev.to/wimadev/guide-deploy-ghost-with-docker-on-sliplane-4c0b</link>
      <guid>https://dev.to/wimadev/guide-deploy-ghost-with-docker-on-sliplane-4c0b</guid>
      <description>&lt;p&gt;&lt;a href="https://ghost.org/" rel="noopener noreferrer"&gt;Ghost&lt;/a&gt; is an open source blogging and newsletter platform designed for professional publishers. In this guide, I want to show you, how you can spin up and deploy your own instance of Ghost using &lt;a href="https://docker.com" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; and &lt;a href="https://sliplane.io?utm_source=ghost" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can find a detailed guide on how to use ghost in the official docs: &lt;a href="https://ghost.org/help/manual/" rel="noopener noreferrer"&gt;https://ghost.org/help/manual/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Login at Sliplane using your GitHub account.&lt;/li&gt;
&lt;li&gt;Click on "Create Project", choose a name for the project and click "Create Project".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Spin Up a MySQL Database
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Click on the new project and then click on "Deploy Service".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you don't have a server yet, click on "Create Server". Select a location, instance type and name for your server and click on "Create Paid Server". The Base server type is selected by default and it should be plenty strong to run your Ghost app. You can always upgrade your server later, if you need more computing power.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5lxv5kruu9gdydtsrvjb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5lxv5kruu9gdydtsrvjb.png" alt=" " width="800" height="965"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After the server has been created, it will show up in the servers list. Click on your server, in order to continue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd92mcrpvfrgi0qlltu60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd92mcrpvfrgi0qlltu60.png" alt=" " width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the pre configured images section, click on the MySQL preset card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgvlxmiv1el7c5sr11ics.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgvlxmiv1el7c5sr11ics.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For additional security, scroll down to "Expose Service" and switch off the "Public" toggle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4f3g78i9tca17o26rc3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4f3g78i9tca17o26rc3.png" alt=" " width="800" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Optionally, you can rename the database service and customize the other settings as well, but we will stick with the defaults.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click "Deploy" and wait for the deploy to finish&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0z5gg5fpf7qgfqxisth.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0z5gg5fpf7qgfqxisth.png" alt=" " width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7zemazd1zykcuwpsendo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7zemazd1zykcuwpsendo.png" alt=" " width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Spin Up Ghost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;After your database has been deployed, navigate to the project where you created it and click "Deploy Service" again&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select the server, where the database is running on&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flbp36vkw7jrp8ipoi286.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flbp36vkw7jrp8ipoi286.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click on "Registry" as the deploy source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frkzxxa09z96rsx9bwth3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frkzxxa09z96rsx9bwth3.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the "Image URL" text field up top, type "Ghost" and choose the official Ghost image from the search results and the version that you want to deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr32bpzvh7wx9p5mjjdrv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr32bpzvh7wx9p5mjjdrv.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add the following environment variables, but make sure to replace placeholders in all caps with your actual config. All database connection settings can be found on the settings page of the database service, that you just deployed earlier.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;database__client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql
&lt;span class="nv"&gt;database__connection__database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_DATABASE_NAME &lt;span class="c"&gt;# replace&lt;/span&gt;
&lt;span class="nv"&gt;database__connection__host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;INTERNAL_DB_HOSTNAME &lt;span class="c"&gt;# replace&lt;/span&gt;
&lt;span class="nv"&gt;database__connection__password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DB_ROOT_PASSWORD &lt;span class="c"&gt;# replace&lt;/span&gt;
&lt;span class="nv"&gt;database__connection__user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;root
&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://&lt;span class="nv"&gt;$SLIPLANE_DOMAIN&lt;/span&gt; &lt;span class="c"&gt;# $SLIPLANE_DOMAIN is a variable that will be populated automatically. You can replace this with a custom domain later, if you plan to add one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Add a new Volume, give it a name of your choice and use &lt;code&gt;/var/lib/ghost/content&lt;/code&gt; as the mount path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn719deooulsdvi2ykwo5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn719deooulsdvi2ykwo5.png" alt=" " width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click "Deploy" and wait for the deploy to finish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7ie57ws4g217247k52s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7ie57ws4g217247k52s.png" alt=" " width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After your deploy is done, you can access Ghost through the public domain that will be displayed in your dashboard.&lt;/p&gt;

&lt;p&gt;That's already it! &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>docker</category>
      <category>sliplane</category>
    </item>
    <item>
      <title>5 Cheap Object Storage Providers</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Tue, 01 Jul 2025 17:30:21 +0000</pubDate>
      <link>https://dev.to/wimadev/5-cheap-object-storage-providers-5hhh</link>
      <guid>https://dev.to/wimadev/5-cheap-object-storage-providers-5hhh</guid>
      <description>&lt;p&gt;Object Storage is an essential component of modern web development. For a long time, AWS S3 was the go-to option for most of us, but nowadays their competition is huge, offering reliable alternatives at a fraction of the price.&lt;/p&gt;

&lt;p&gt;We researched some providers to use in our cloud platform &lt;a href="https://sliplane.io/?utm_source=5-object-storage" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; some time ago and I want to share our top picks with you.&lt;/p&gt;

&lt;p&gt;Here are 5 cheap object storage providers you can consider as an AWS S3 alternative.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; You might have very specific requirements for your object storage solution. For this article, I only include providers in the list of known brands with overall good reputation and we are mainly comparing prices here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Storage Cost&lt;/th&gt;
&lt;th&gt;Egress Cost&lt;/th&gt;
&lt;th&gt;Durability&lt;/th&gt;
&lt;th&gt;Availability SLA&lt;/th&gt;
&lt;th&gt;Free Tier&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backblaze B2&lt;/td&gt;
&lt;td&gt;$0.006 / GB / month&lt;/td&gt;
&lt;td&gt;Free up to 3× storage, then $0.01 / GB&lt;/td&gt;
&lt;td&gt;99.999999999% (11 nines)&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Widely adopted, solid reliability, easy S3 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasabi&lt;/td&gt;
&lt;td&gt;$0.00699 / GB / month&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;99.999999999% (11 nines)&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Requires 90-day minimum retention, fully S3-compatible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare R2&lt;/td&gt;
&lt;td&gt;$0.015 / GB / month&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;99.999999999% (11 nines)&lt;/td&gt;
&lt;td&gt;depends on downtime&lt;/td&gt;
&lt;td&gt;10 GB storage + 1M writes + 10M reads/month&lt;/td&gt;
&lt;td&gt;Pays for read/write ops, strong edge caching network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner Object Storage&lt;/td&gt;
&lt;td&gt;$0.00713 / GB / month&lt;/td&gt;
&lt;td&gt;$0.00143 / GB&lt;/td&gt;
&lt;td&gt;Not stated&lt;/td&gt;
&lt;td&gt;Not stated&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Great price/performance, limited data centers, restrictive account policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MinIO&lt;/td&gt;
&lt;td&gt;Depends on infra&lt;/td&gt;
&lt;td&gt;Depends on infra&lt;/td&gt;
&lt;td&gt;Depends on setup&lt;/td&gt;
&lt;td&gt;Depends on setup&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Self-hosted, open-source, requires setup and maintenance, durability is debatable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. &lt;a href="https://www.backblaze.com/cloud-storage" rel="noopener noreferrer"&gt;Backblaze B2&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9syl6snz1stoukvgbrxa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9syl6snz1stoukvgbrxa.png" alt="Backblaze Logo" width="360" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Backblaze B2 offers reliable, low-cost object storage with an easy-to-use S3-compatible API. The &lt;a href="https://www.backblaze.com/cloud-storage/pricing" rel="noopener noreferrer"&gt;storage price&lt;/a&gt; is very affordable at $0.006/GB/month, and egress is free up to 3× your storage volume per month, then $0.01/GB for additional egress.&lt;/p&gt;

&lt;p&gt;Backblaze is widely adopted and a solid choice when it comes to &lt;a href="https://www.backblaze.com/docs/cloud-storage-resiliency-durability-and-availability" rel="noopener noreferrer"&gt;durability&lt;/a&gt; (99.999999999% annual durability) and general &lt;a href="https://www.backblaze.com/company/policy/sla" rel="noopener noreferrer"&gt;reliability&lt;/a&gt; (99.9% uptime SLA). We use it at &lt;a href="https://sliplane.io/?utm_source=5-object-storage" rel="noopener noreferrer"&gt;sliplane.io&lt;/a&gt; for example for storing backups but also to store configuration and init scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;a href="https://wasabi.com/" rel="noopener noreferrer"&gt;Wasabi&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw69qvocrzenil1vkqikt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw69qvocrzenil1vkqikt.png" alt="Wasabi Logo" width="426" height="130"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With $0.00699/GB/month, storage is only slightly more expensive than on Backblaze, but egress is completely free on &lt;a href="https://wasabi.com/" rel="noopener noreferrer"&gt;Wasabi&lt;/a&gt;. &lt;a href="https://docs.wasabi.com/v1/docs/how-durable-is-wasabi" rel="noopener noreferrer"&gt;Durability&lt;/a&gt; is given at 11 nines as well and in their &lt;a href="https://wasabi.com/legal/sla" rel="noopener noreferrer"&gt;SLA&lt;/a&gt; they start discounting you if availability of their service drops below 99.9%.&lt;/p&gt;

&lt;p&gt;Wasabi is fully S3-compatible, but it requires a 90-day minimum retention period on stored data when using their pay-as-you-go pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;a href="https://www.cloudflare.com/r2/" rel="noopener noreferrer"&gt;Cloudflare R2&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstg8holjuszf5b8d5z1c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstg8holjuszf5b8d5z1c.png" alt="Cloudflare R2 Logo" width="542" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similar to Wasabi, Cloudflare R2 provides object storage with zero egress fees as well, which is ideal for applications with heavy outbound data transfers. Storage costs are higher than Wasabi's at $0.015/GB/month, but still much cheaper compared to &lt;a href="https://aws.amazon.com/s3/pricing/" rel="noopener noreferrer"&gt;AWS S3&lt;/a&gt; which comes at $0.023/GB/month. They also have a free tier for the first 10GB of storage, making it ideal for small projects. While egress is free, you pay for read ($0.36 / M) and write ($4.50 / M) operations, although Cloudflare has a free tier of 1M writes and 10M reads per month as well.&lt;/p&gt;

&lt;p&gt;A main benefit of R2 is access to Cloudflare's edge caching network, giving you fast global access to your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;a href="https://www.hetzner.com/storage/object-storage" rel="noopener noreferrer"&gt;Hetzner Object Storage&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn64g244prd1zphn97gs2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn64g244prd1zphn97gs2.png" alt="Hetzner Logo" width="378" height="86"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hetzner provides a great European alternative for Object storage. Although their service is comparably new and when we tested it in beta we stumbled into issues, which they probably got all sorted by now, Hetzner is known to provide great service at an exceptional price/performance ratio.&lt;/p&gt;

&lt;p&gt;At $0.00713/GB/month storage costs are very competitive and especially their $0.00143/GB egress pricing can be an attractive alternative to AWS where egress costs are about 60x higher. Its downsides include limited global data center locations and it can be tricky to open an account at Hetzner since they are very restrictive as part of their effort to keep scammers in check.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;a href="https://min.io/" rel="noopener noreferrer"&gt;MinIO&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibu9wc5uzzqdjv1wz1nj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibu9wc5uzzqdjv1wz1nj.png" alt="MinIO Logo" width="296" height="90"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MinIO is a self-hosted, open-source object storage software fully compatible with the S3 API. Since it runs on your own servers or cloud, storage fees—costs depend on your infrastructure but you must also include the time it takes to set up and maintain your object storage solution.&lt;/p&gt;

&lt;p&gt;Is it a good idea to &lt;a href="https://sliplane.io/blog/5-things-that-should-be-illegal-to-selfhost" rel="noopener noreferrer"&gt;self-host object storage&lt;/a&gt;? That's debatable. An important point of good object storage is high durability and achieving the same 11 nines (99.999999999%) as other providers offer at a similar price is going to be tough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Hope any of that helped! Each of these providers offers different advantages depending on your specific needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backblaze B2&lt;/strong&gt; is great for reliable, cost-effective storage with reasonable egress allowances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wasabi&lt;/strong&gt; excels when you need unlimited free egress but can commit to 90-day retention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare R2&lt;/strong&gt; is perfect for applications that benefit from global edge caching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner&lt;/strong&gt; offers excellent European-focused pricing with competitive egress costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MinIO&lt;/strong&gt; gives you complete control but requires significant setup and maintenance effort&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When choosing an object storage provider, consider not just the storage costs but also egress fees, your geographic requirements, integration complexity, and the total cost of ownership including your time investment.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cheers,&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lukas&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Co-Founder &lt;a href="https://sliplane.io/?utm_source=5-object-storage" rel="noopener noreferrer"&gt;sliplane.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>aws</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How to add Bug Tracking to your Apps (Free Open Source Tool) - Bugsink 🐞</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Wed, 25 Jun 2025 12:31:22 +0000</pubDate>
      <link>https://dev.to/wimadev/how-to-add-bug-tracking-to-your-apps-free-tool-3l88</link>
      <guid>https://dev.to/wimadev/how-to-add-bug-tracking-to-your-apps-free-tool-3l88</guid>
      <description>&lt;p&gt;&lt;a href="https://www.bugsink.com/" rel="noopener noreferrer"&gt;Bugsink&lt;/a&gt; is a free and open source bug tracking solution that allows you to collect all errors that are happening across your applications in a central place.&lt;/p&gt;

&lt;p&gt;I tried a few open source bug trackers, but from what I found they look either very old (&lt;a href="https://www.bugzilla.org/" rel="noopener noreferrer"&gt;https://www.bugzilla.org/&lt;/a&gt;) or super heavy (&lt;a href="https://sentry.io" rel="noopener noreferrer"&gt;https://sentry.io&lt;/a&gt;). Bugsink provides a nice middle ground so I encourage you to give it a try.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll spin up our own instance of Bugsink using &lt;a href="https://sliplane.io?utm_source=dev-bugsink" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Tutorial
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/bBpAct3HumE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Spin up a Database
&lt;/h2&gt;

&lt;p&gt;You can find detailed installation instructions in the &lt;a href="https://www.bugsink.com/docs/installation/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;. We will choose the Docker installation and use a &lt;a href="https://www.mysql.com/" rel="noopener noreferrer"&gt;MySQL&lt;/a&gt; database as a persistent data store.&lt;/p&gt;

&lt;p&gt;We will start by spinning up a database:&lt;/p&gt;

&lt;p&gt;1.) Go to &lt;a href="https://sliplane.io?utm_source=dev-bugsink" rel="noopener noreferrer"&gt;sliplane.io&lt;/a&gt; and sign in with your &lt;a href="https://github.com" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; account&lt;br&gt;
2.) Click on "Create Project" and give it a name of your choice. I will choose "Bugsink" as a project name.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcd9sd33phv4a6qlxhfju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcd9sd33phv4a6qlxhfju.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3.) Click on the project and hit "Deploy Service"&lt;br&gt;
4.) If you don't have a server yet, click on "Create Server" and choose a location and server type. The default server type is plenty strong to run MySQL and Bugsink and you can scale it up later so I recommend starting with the default. Choose a name for your server and hit "Create Paid Server".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0h6559io2ngzi53uanay.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0h6559io2ngzi53uanay.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;5.) Once the server has been created and shows up in the list, you can select it to continue&lt;br&gt;
6.) Choose "MySQL" from the pre configured image list&lt;br&gt;
7.) On the deploy settings screen, you can apply some optional settings: Disable "public" for more security, change the database name to "bugsink" and rename your service "Bugsink MySQL". These settings are optional but recommended.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9yxp12c60sfr9gfeulv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9yxp12c60sfr9gfeulv.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;8.) Hit deploy and wait for the deploy to finish until you see a green dot lighting up, which indicates that your MySQL database is ready to accept connections.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7ofltqrlcq2bl78lpuv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7ofltqrlcq2bl78lpuv.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Deploy Bugsink
&lt;/h2&gt;

&lt;p&gt;Next up, we will deploy Bugsink.&lt;/p&gt;

&lt;p&gt;1.) In the Bugsink project, click "Deploy Service" again.&lt;br&gt;
2.) Choose the server, where your database is running on&lt;br&gt;
3.) Click on deploy from "Registry"&lt;br&gt;
4.) In the "Image URL" input, search for "bugsink" and choose the official bugsink image from the dropdown as well as the version of bugsink that you want to install. It's recommended to use a pinned version in production to avoid unintentional upgrades of the service.&lt;br&gt;
5.) Add the following environment variables. Click on "from .env file" and paste in this content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;a 50 chars long secret key]
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql://[user]:[password]@[host]/[name]
&lt;span class="nv"&gt;CREATE_SUPERUSER&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;admin username]:[admin password]
&lt;span class="nv"&gt;BEHIND_HTTPS_PROXY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;your sliplane.app url]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; These environment values contain placeholders! you need to replace everything that is inside square brackets &lt;code&gt;[&lt;/code&gt; &lt;code&gt;]&lt;/code&gt; with actual values! You can find all database connection settings in the MySQL Sliplane service. The &lt;code&gt;CREATE_SUPERUSER&lt;/code&gt; name and password are arbitrary. The &lt;code&gt;SECRET_KEY&lt;/code&gt; as well, but it has to be 50 chars long and needs uppercase and lowercase characters and numbers. The &lt;code&gt;BASE_URL&lt;/code&gt; can be found in the settings of your "Bugsink" app on Sliplane after your first deploy, so make sure to update this value after you deployed the app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7xaef6x2zd8gupfzfa1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7xaef6x2zd8gupfzfa1.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;6.) Optionally, change the name to "Bugsink" and hit "Deploy".&lt;/p&gt;

&lt;p&gt;After a little while, your application should be deployed and the green light should be visible. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1sbl5i6jzn9pzuixul7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1sbl5i6jzn9pzuixul7.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Great! &lt;/p&gt;

&lt;p&gt;We can test if everything is working, by navigating to the "Public Domain" that is displayed in your "Bugsink" Sliplane app and login with the superuser credentials that we passed as environment variables when we deployed the application.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Once the superuser has been created, you can not overwrite the credentials via envs!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Instrument your App using the Sentry SDK
&lt;/h2&gt;

&lt;p&gt;Instrumenting means, we will inject statements in our code, that send errors to our backend. Depending on your stack, there are tools that do this automatically for you, for example &lt;a href="https://sentry.io" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sentry is an open source bugtracker itself, but it is pretty heavy and we can just use their client side SDK for instrumentation and send the data to Bugsink.  &lt;/p&gt;

&lt;p&gt;Before we can get started with instrumenting our apps, we need to create a new Team and a new Project in Bugsink first: &lt;/p&gt;

&lt;p&gt;1.) Click on "Teams" and then "New Team". Give it a name and visibility setting of your choice and click on "Save"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8e9daauic26vb8kwk3pl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8e9daauic26vb8kwk3pl.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2.) Click on "Projects" and then "New Project". Fill out the form and hit "Save" as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb065eenkphfo5mvvn99j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb065eenkphfo5mvvn99j.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After saving your project, you should see some installation instructions to connect a Sentry client to your Bugsink instance. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg6n5oe0acn2ipf93h03u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg6n5oe0acn2ipf93h03u.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: If you see "localhost" in the DSN, you need to update the "BASE_URL" in the environment variables of your Bugsink app on Sliplane first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You will also see a link to &lt;a href="https://docs.sentry.io/platforms/" rel="noopener noreferrer"&gt;https://docs.sentry.io/platforms/&lt;/a&gt; where you can find instructions on how to add Sentry to the application that you want to instrument. Sentry has an SDK for almost all major frameworks, so choose the framework that you use in your application and make sure to follow their installation guide step by step.&lt;/p&gt;

&lt;p&gt;No matter what SDK you are choosing, you want to configure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The DSN that is displayed on the Bugsink installation instruction page&lt;/li&gt;
&lt;li&gt;Configuration for uploading source maps which contains:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;org: &lt;span class="o"&gt;[&lt;/span&gt;your bugsink team name],
project: &lt;span class="o"&gt;[&lt;/span&gt;your bugsink project name],
authToken: &lt;span class="o"&gt;[&lt;/span&gt;auth token from bugsink],
url: &lt;span class="o"&gt;[&lt;/span&gt;the public domain of your Bugink instance on Sliplane, including https],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can generate authTokens in Bugsink by click on "Tokens" in the navbar on top. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6otoley1sambp697aegz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6otoley1sambp697aegz.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also makes sense to add a &lt;code&gt;release&lt;/code&gt; version to the sentry config, but you can find out more about what's possible in their official documentation.&lt;/p&gt;

&lt;p&gt;That's basically it! You can test your setup by throwing errors in your code like this for example in javascript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error should be collected in your Bugsink instance!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2ejb36afhgum1odnthy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2ejb36afhgum1odnthy.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you liked this little tutorial, feel free to like, comment and subscribe and I see you next time!&lt;/p&gt;

&lt;p&gt;Lukas&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>docker</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Setup SSL/TLS for PostgreSQL in Docker</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Tue, 17 Jun 2025 16:02:26 +0000</pubDate>
      <link>https://dev.to/wimadev/setup-ssltls-for-postgresql-with-docker-g15</link>
      <guid>https://dev.to/wimadev/setup-ssltls-for-postgresql-with-docker-g15</guid>
      <description>&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;If you want to run PostgreSQL in production, setting up &lt;a href="https://csrc.nist.gov/glossary/term/transport_layer_security" rel="noopener noreferrer"&gt;Transport Layer Security&lt;/a&gt; (TLS) is a must in order to prevent &lt;a href="https://csrc.nist.gov/glossary/term/man_in_the_middle_attack" rel="noopener noreferrer"&gt;man-in-the-middle attacks&lt;/a&gt;. In this step-by-step guide, I will show you how you can connect to your database securely using your own local &lt;a href="https://csrc.nist.gov/glossary/term/certificate_authority" rel="noopener noreferrer"&gt;Certificate Authority&lt;/a&gt;. If you need an in-depth explanation of all settings, you can check out the official documentation on how to &lt;a href="https://www.postgresql.org/docs/current/ssl-tcp.html" rel="noopener noreferrer"&gt;set up TLS in PostgreSQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this example, we will &lt;em&gt;ONLY&lt;/em&gt; allow encrypted connections and we will set &lt;code&gt;sslmode&lt;/code&gt; to &lt;code&gt;verify-full&lt;/code&gt;, which is the strictest possible &lt;a href="https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-CONNECT-SSLMODE" rel="noopener noreferrer"&gt;connection setting in PostgreSQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To follow along, check out the example repository over here: &lt;a href="https://github.com/sliplane-support/postgres-tls" rel="noopener noreferrer"&gt;PostgreSQL with TLS repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need &lt;a href="https://docker.com" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; installed on your machine. If you are on a Mac or Windows, make sure to install &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt; and have it running in the background. Verify Docker is running by typing &lt;code&gt;docker ps&lt;/code&gt; in your terminal. You should see a list of running containers or at least no error if Docker is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Tutorial
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fRbALTUajZo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Generate Certificates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Generate a Certificate Authority (CA)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: All certificate files that are generated in the following steps (CA, server, and client) should be treated as secrets. Do not add them to your git repository, do not log them, and do not bake them into your Docker image (use runtime envs only). If necessary, add them to your &lt;code&gt;.gitignore&lt;/code&gt; file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, we create a root CA key and certificate that will sign our server and client certs. You can change the name if you want. Our CA will be valid for 365 days and needs to be renewed after that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; rootCA.key 2048
openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; rootCA.key &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-out&lt;/span&gt; rootCA.crt &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=MyLocalCA"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create Server Certificate Files
&lt;/h3&gt;

&lt;p&gt;Generate a key and CSR (Certificate Signing Request), then sign it using the CA:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Important&lt;/em&gt;: Here you need to provide the domain name your PostgreSQL instance will be running on. In the example, I use &lt;code&gt;localhost&lt;/code&gt;, but make sure to swap out the hostname if you use another one.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; server.key 2048
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; server.key &lt;span class="nt"&gt;-out&lt;/span&gt; server.csr &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=localhost"&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; server.csr &lt;span class="nt"&gt;-CA&lt;/span&gt; rootCA.crt &lt;span class="nt"&gt;-CAkey&lt;/span&gt; rootCA.key &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; server.crt &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-sha256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create Client Certificate Files
&lt;/h3&gt;

&lt;p&gt;This certificate will be used by the client to authenticate. Only certificates that have been signed by the same certificate authority as the server certificate will work.&lt;/p&gt;

&lt;p&gt;To create the client certificate, we follow the same steps as for the server. As a common name, I chose the postgres user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; client.key 2048
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; client.key &lt;span class="nt"&gt;-out&lt;/span&gt; client.csr &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=postgres"&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; client.csr &lt;span class="nt"&gt;-CA&lt;/span&gt; rootCA.crt &lt;span class="nt"&gt;-CAkey&lt;/span&gt; rootCA.key &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; client.crt &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-sha256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Create Config Scripts to Enable TLS in PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Create a new file in the root of the project and name it &lt;code&gt;ssl-config.sh&lt;/code&gt;. The script contains instructions to enable ssl in PostgreSQL and enforce TLS for all connections with sslmode set to &lt;code&gt;verify-full&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to allow insecure connections, you can omit the "Force SSL" section of the script or change the sslmode to something more forgiving.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Configure PostgreSQL to use SSL&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ssl = on"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/lib/postgresql/data/postgresql.conf
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ssl_cert_file = '/var/lib/postgresql/server.crt'"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/lib/postgresql/data/postgresql.conf
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ssl_key_file = '/var/lib/postgresql/server.key'"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/lib/postgresql/data/postgresql.conf
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ssl_ca_file = '/var/lib/postgresql/rootCA.crt'"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/lib/postgresql/data/postgresql.conf

&lt;span class="c"&gt;# Enforce SSL for all connections&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hostssl all all all cert clientcert=verify-full"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/lib/postgresql/data/pg_hba.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create another file in the root of the project and call it &lt;code&gt;entrypoint.sh&lt;/code&gt;. This script copies our certificates into the container at runtime before the default entrypoint is executed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Add certificate files&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVER_CRT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/lib/postgresql/server.crt
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVER_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/lib/postgresql/server.key
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_CA_CRT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/lib/postgresql/rootCA.crt

&lt;span class="c"&gt;# Update file permissions of certificates&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /var/lib/postgresql/server.&lt;span class="k"&gt;*&lt;/span&gt; /var/lib/postgresql/rootCA.crt
&lt;span class="nb"&gt;chown &lt;/span&gt;postgres:postgres /var/lib/postgresql/server.&lt;span class="k"&gt;*&lt;/span&gt; /var/lib/postgresql/rootCA.crt

&lt;span class="c"&gt;# Run the base entrypoint&lt;/span&gt;
docker-entrypoint.sh postgres 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script requires 3 environment variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SERVER_CRT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SERVER_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ROOT_CA_CRT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The contents of these envs is the plaintext content of the 3 certificate files: &lt;code&gt;server.crt&lt;/code&gt;, &lt;code&gt;server.key&lt;/code&gt;, and &lt;code&gt;rootCA.crt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Build a Docker Image with TLS Enabled
&lt;/h2&gt;

&lt;p&gt;Create a new file in your project and name it &lt;code&gt;Dockerfile&lt;/code&gt;. We will  base the Docker image on &lt;code&gt;postgres:17.5&lt;/code&gt;, then copy our custom &lt;code&gt;ssl-config.sh&lt;/code&gt; and &lt;code&gt;entrypoint.sh&lt;/code&gt; into the image, make it executable and replace the default entrypoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start from postgres 17.5 as a base image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; postgres:17.5&lt;/span&gt;

&lt;span class="c"&gt;# Copy ssl-config script which runs on first startup&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ssl-config.sh /docker-entrypoint-initdb.d/ssl-config.sh&lt;/span&gt;

&lt;span class="c"&gt;# Copy the entrypoint script to the image&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; entrypoint.sh /usr/local/bin/entrypoint.sh&lt;/span&gt;

&lt;span class="c"&gt;# Make the entypoint script executable&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/entrypoint.sh

&lt;span class="c"&gt;# Set the entrypoint&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; [ "/usr/local/bin/entrypoint.sh"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Connect to the instance
&lt;/h2&gt;

&lt;p&gt;To see if everything is working, we will try to build and run our image locally and try to connect to our instance. We will use the &lt;code&gt;psql&lt;/code&gt; client to check if we can connect to our instance. If you have not installed it on your machine, make sure to add it first using your package manager of choice, e.g. &lt;code&gt;brew install postgresql&lt;/code&gt; on macOS.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This local test will only work if you previously set the common name of your server certificate to &lt;code&gt;localhost&lt;/code&gt;. Otherwise, you need to create a new server certificate with localhost as a common name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Build the docker image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; postgres-tls &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;SERVER_CRT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;server.crt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;SERVER_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;server.key&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ROOT_CA_CRT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;rootCA.crt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-p&lt;/span&gt; 5432:5432 &lt;span class="nt"&gt;--name&lt;/span&gt; postgres-tls postgres-tls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Make sure to use the correct path to your server.crt, server.key, and rootCA.crt file if you moved these files around.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Test the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This command should get you into the PostgreSQL shell:&lt;/span&gt;
psql &lt;span class="s2"&gt;"host=localhost dbname=postgres user=postgres sslmode=verify-full sslrootcert=rootCA.crt sslcert=client.crt sslkey=client.key"&lt;/span&gt;

&lt;span class="c"&gt;# This should fail, because we enforce tls:&lt;/span&gt;
psql &lt;span class="s2"&gt;"host=localhost dbname=postgres user=postgres sslmode=disable"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Important: Make sure to run this from the same directory where your certificates are, or update the file paths accordingly.&lt;br&gt;
Also make sure the permissions on client.key are set to 600. You can update the permissions using &lt;code&gt;chmod 600 client.key&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Deploy
&lt;/h2&gt;

&lt;p&gt;In order to make the database available over the internet, we will deploy it on &lt;a href="https://sliplane.io?utm_source=postgres-ssl" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sliplane is an affordable cloud provider, that makes deployment and managing containerized applications very easy.&lt;/p&gt;

&lt;p&gt;1.) Create a GitHub repository with the two files described above: &lt;code&gt;entrypoint.sh&lt;/code&gt; from step 3 and &lt;code&gt;Dockerfile&lt;/code&gt; from step 4. I created an example repo that you can fork here: &lt;a href="https://github.com/sliplane-support/postgres-tls" rel="noopener noreferrer"&gt;PostgreSQL with TLS repository&lt;/a&gt;&lt;br&gt;
2.) Log in to &lt;a href="https://sliplane.io?utm_source=postgres-ssl" rel="noopener noreferrer"&gt;Sliplane&lt;/a&gt; with your GitHub account&lt;br&gt;
3.) Create a new Project and click on "Deploy Service"&lt;br&gt;
4.) Create a new Server, where your PostgreSQL instance will be running - you can easily start with the base server and scale up later if you need to&lt;br&gt;
5.) Choose "Repository" as the deploy source&lt;br&gt;
6.) Choose your PostgreSQL repository from the dropdown. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the PostgreSQL repository does not show up in the list, you need to hit "Configure Repository Access" first in order to grant Sliplane access to deploy the repo&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;7.) In the "Expose Service" section, change the protocol to TCP&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvtoqu7jtl7g7n3ddh4mp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvtoqu7jtl7g7n3ddh4mp.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;8.) In the "Environment Variables" section, add&lt;/p&gt;

&lt;p&gt;POSTGRES_PASSWORD - arbitrary secret&lt;br&gt;
SERVER_CRT - contents of your server.crt file&lt;br&gt;
SERVER_KEY - contents of your server.key file&lt;br&gt;
ROOT_CA_CRT - contents of your rootCA.crt file&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faqzy0kz9ors210q1bp1a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faqzy0kz9ors210q1bp1a.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;9.) In the "Volumes" section, add a new volume with a name of your choice and choose &lt;code&gt;/var/lib/postgresql/data&lt;/code&gt; as the mount path&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft67v92e0191gxpjz6n39.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft67v92e0191gxpjz6n39.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;10.) Hit "Deploy". &lt;/p&gt;

&lt;p&gt;Here you can see an overview of how the settings should look:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdngq2s39ycucf0c0ue15.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdngq2s39ycucf0c0ue15.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the deploy we get issued a &lt;code&gt;sliplane.app&lt;/code&gt; domain that we can use in our server certificate. Alternatively, you could use a custom domain in your certificate and attach that domain to your service afterwards.&lt;/p&gt;

&lt;p&gt;11.) Back in your terminal, create a new server certificate as described above and use your &lt;code&gt;sliplane.app&lt;/code&gt; domain as a common name. You can find the domain in the service settings of your newly created service under &lt;code&gt;Public Domain&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwsy9857hbkfmjsz7udfy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwsy9857hbkfmjsz7udfy.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; server.key 2048
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; server.key &lt;span class="nt"&gt;-out&lt;/span&gt; server.csr &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=...sliplane.app"&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; server.csr &lt;span class="nt"&gt;-CA&lt;/span&gt; rootCA.crt &lt;span class="nt"&gt;-CAkey&lt;/span&gt; rootCA.key &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; server.crt &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-sha256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: Your service needs to be public when you create it. Otherwise, it won't get a public domain (see screenshot above)&lt;br&gt;
Make sure to replace the &lt;code&gt;\CN=...&lt;/code&gt; with your own sliplane.app domain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;12.) Replace the &lt;code&gt;SERVER_CRT&lt;/code&gt; environment variable of your Sliplane service with the new certificate. After you hit save, a new deploy will be triggered.&lt;/p&gt;

&lt;p&gt;That's it! You now have access to a PostgreSQL instance via a secure connection. You can test it by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="s2"&gt;"host=YOUR_APP.sliplane.app dbname=postgres user=postgres sslmode=verify-full sslrootcert=rootCA.crt sslcert=client.crt sslkey=client.key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: Replace the sliplane.app domain that you have been issued and that is used in your certificate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you liked this tutorial, feel free to comment, like, and share.&lt;/p&gt;

&lt;p&gt;Thanks!&lt;/p&gt;

&lt;p&gt;Lukas&lt;/p&gt;

</description>
      <category>docker</category>
      <category>postgres</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How Hard Can It Be? „Building a Photo Sharing App is easy!“ (*regrets 😓)</title>
      <dc:creator>Lukas Mauser</dc:creator>
      <pubDate>Sat, 07 Jun 2025 18:11:59 +0000</pubDate>
      <link>https://dev.to/wimadev/how-hard-can-it-be-building-a-photo-sharing-app-is-easy-19mo</link>
      <guid>https://dev.to/wimadev/how-hard-can-it-be-building-a-photo-sharing-app-is-easy-19mo</guid>
      <description>&lt;p&gt;Recently, I was looking for a simple way to share photos amongst friends and family. But no way I would pay $14.99 a month for a turnkey solution! - This goes against rule #422 of being a developer: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule #422:&lt;/strong&gt; I'm a developer, I can build that myself - how hard can it be?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After all, it's just uploading and displaying some photos...&lt;/p&gt;

&lt;p&gt;A friend already helped me out with a prototype, but things quickly started to break once I added 3000 high resolution images. &lt;/p&gt;

&lt;p&gt;One important lesson before we get into it:&lt;/p&gt;

&lt;p&gt;Just pay for the damn service! Don't be so cheap. I spend 5 days on what I estimated to be a 5 hour project. &lt;/p&gt;

&lt;p&gt;Rule #675 of being a developer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule #675:&lt;/strong&gt; Your estimates will be wrong. By a lot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay it was still fun, so here are some lessons: &lt;/p&gt;

&lt;h2&gt;
  
  
  1. Where to store your images
&lt;/h2&gt;

&lt;p&gt;The prototype was build on Cloudflare's object storage R2. Cloudflare also offers an image service, that comes with compression and resizing - 5,000 transformations are included, after that it's $5 per 100,000 images stored per month and $1 per 100,000 images delivered per month. I won't pay for that - I'm a developer, I can do that myself, how hard can it be?&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Smooth scrolling through thousands of images
&lt;/h2&gt;

&lt;p&gt;The trivial solution - render 3000 images on your site.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"image in images"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"image.id"&lt;/span&gt; &lt;span class="na"&gt;:src=&lt;/span&gt;&lt;span class="s"&gt;"image.src"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In general, I'd recommend to always start with the trivial solution and work your way up. Btw. I am building this with Nuxt 3 - so example code will be Nuxt, Vue and TypeScript.&lt;/p&gt;

&lt;p&gt;At a few hundred images the browser started to hickup and scrolling wasn't smooth anymore. In very large lists, it is common practice to work with a virtual dom and only render visible elements.&lt;/p&gt;

&lt;p&gt;I found &lt;a href="https://github.com/Akryum/vue-virtual-scroller/tree/master" rel="noopener noreferrer"&gt;vue-virtual-scroller&lt;/a&gt; but for reasons I can't remember, it did not work immediately so I got frustrated and went back to rule #422: &lt;/p&gt;

&lt;p&gt;I can build that!&lt;/p&gt;

&lt;p&gt;I started with rendering empty divs instead of imgs. All devices i tested with had no issue to handle thousands of divs and scrolling through worked smoothly. Might be an issue on old devices, but we have to draw a line somewhere...&lt;/p&gt;

&lt;p&gt;I used an intersection observer to keep track of visible elements on screen, and replaced my divs with imgs whenever they entered the viewport:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
 &lt;span class="c"&gt;&amp;lt;!--...--&amp;gt;&lt;/span&gt;

 &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"(image,index) in images"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"image.id"&lt;/span&gt; &lt;span class="na"&gt;:data-index=&lt;/span&gt;&lt;span class="s"&gt;"index"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"intersectingIndices.has(index)"&lt;/span&gt; &lt;span class="na"&gt;:src=&lt;/span&gt;&lt;span class="s"&gt;"image.src"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;


&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="nf"&gt;onMounted&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;{
   const observer = new IntersectionObserver(handleIntersection, {
      root:null, // Use the viewport as the root
      threshold: 0.1, // Trigger when 10% of the image is visible
   });
})

const intersectingIndices = new Set&lt;span class="nt"&gt;&amp;lt;number&amp;gt;&lt;/span&gt;();
function handleIntersection(entries: IntersectionObserverEntry[]) {
  for (const entry of entries) {
    const indexAttr = entry.target.getAttribute("data-index");
    if (indexAttr == null) continue;
    const index = Number(indexAttr);
    if (entry.isIntersecting) {
      intersectingIndices.add(index);
    } else {
      intersectingIndices.delete(index);
    }
  }
}
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked, but scrolling was stuttering. With the research and previous failed attempts, I am approaching the 5 hours deadline. Luckily, we live in modern times and ChatGPT pointed me towards batching DOM updates with &lt;code&gt;requestAnimationFrame(applyPendingUpdates)&lt;/code&gt;. It worked!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleIntersection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IntersectionObserverEntry&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;indexAttr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data-index&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indexAttr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indexAttr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;intersectingIndices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;intersectingIndices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Batch the update to visibleImages using requestAnimationFrame&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rafId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rafId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;applyPendingVisibleUpdates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beautiful. &lt;/p&gt;

&lt;p&gt;Scrolling now worked buttery smooth. Praise the AI lords!&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Improve Image Load time
&lt;/h2&gt;

&lt;p&gt;Next issue, I could scroll, but the unprocessed images were too large and load time was whack. I used &lt;a href="https://nuxt.com/modules/image" rel="noopener noreferrer"&gt;Nuxt Image&lt;/a&gt; in order to compress and downsize the images on the fly. Nuxt Image ships with a builtin image transformation library that can be used locally, so F U Cloudflare image service! &lt;/p&gt;

&lt;p&gt;Well, not so fast. CPU and Memory spiked on my server with frequent image transformations. For my limited user count the app handled it well, but at some point this is something to keep in mind.&lt;/p&gt;

&lt;p&gt;I assumed Nuxt image would store transformed files in memory, so my strategy to keep the cache alive was: don’t code bugs so I don’t have to deploy and everything is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Honorable Bug Mentions and Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Back Button
&lt;/h3&gt;

&lt;p&gt;I knew ahead of time, that the app would probably be used 90% on mobile. What I did not know: Some phones have a back button and it get's used a lot. The app could show full res images, but it relied on in memory variables to trigger the detail view. A click on the back button sent users to the homepage and they had to navigate back to the images page and scroll through 1000 images again to get back to where they started. I had to rewrite that to show the detail view on a new page while still preserving the scroll position (also if the page was reloaded).&lt;/p&gt;

&lt;h2&gt;
  
  
  Swiping
&lt;/h2&gt;

&lt;p&gt;Another mobile issues. Users are just used to swiping gestures on their phones. To view the next image, to close the detail view. I tried to make it work in the beginning, but it quickly turned out to be a bad idea, since gestures overlapped with browser native gestures (swipe down to reload, swipe right to navigate back). After a few hours of fine tuning I had my solution: Users need to unlearn their swiping behavior. It's a won't fix. Guess there still is some UX benefit in native apps compared to web.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nuxt
&lt;/h2&gt;

&lt;p&gt;I really like Nuxt and have been reliably using the framework for years. It's just natural, that before you assume a bug in your framework, you usually spent a lot of time to question your own capabilities and whether you chose the wrong career. I spent about 4 hours testing the app from front to back until I figured out, there was a bug in Nuxt, that caused my app to crash. Luckily, they released a new version just 2 days earlier, that fixed it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Cache
&lt;/h2&gt;

&lt;p&gt;To save on storage and bandwidth, I used a custom script to convert all of my images to webp before uploading them to Cloudflare. I used &lt;a href="https://github.com/lovell/sharp" rel="noopener noreferrer"&gt;sharp&lt;/a&gt; for the image transformation. Every now and then you take a picture with your phone and it is rotated 90 degrees. The orientation is stored in the metadata of the image. Sharp strips all metadata by default. I only noticed this, after my images had been uploaded to Cloudflare, so I deleted the bucket, transformed everything again and reuploaded my images (side info: I was visiting relatives at that time. The router is in the basement, I was on first floor (European first floor, American second), I measured 2 MBit/s upload - You do the calculation for 10GB of images...).&lt;/p&gt;

&lt;p&gt;After reuploading the images were still rotated and I spend hours comparing metadata, clearing browser cache and going through reuploading again and again (this time with smaller sample sizes) until I noticed, that the old images were still served from Cloudflare cache. Be smarter than I was. Don't forget the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Photo Upload
&lt;/h2&gt;

&lt;p&gt;I gave my friends an upload button. And they uploaded. Or at least they tried. Some of them tried to upload a lot of images at once - I had no restrictions. Who needs restrictions anyways? Aren't we all born as free human beings? If you asked me, I wouldn't put a restriction on your uploads. But I should have. These big uploads reliably pushed the server into memory limits and broke the application. How did I fix it? I ran the app on localhost and uploaded the images myself. LOL. &lt;/p&gt;

&lt;h2&gt;
  
  
  5. Deploying
&lt;/h2&gt;

&lt;p&gt;Okay, I have to tell ahead of time, I am biased here. But this is just the perfect project to run on &lt;a href="https://sliplane.io?utm_source=image-app" rel="noopener noreferrer"&gt;sliplane.io&lt;/a&gt;. Sliplane is a cheap and dead simple way to run Docker containers on a VPS - no DevOps required. This means we can basically run whatever we want (frontend and backend with image processing) and my cache in memory strategy can actually work for a while! (At least until the machine will be rebooted.) Take that serverless!&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;It's a rabbit hole and it's deeper than it looks from the outside. Don't be cheap. Pay for that damn photo sharing service.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>vue</category>
    </item>
  </channel>
</rss>
