NextCloud Docker Tutorial – Part I

Some time ago, I went on an adventure to try out NextCloud.  During my adventure, I struggled with getting certain aspects of NextCloud working and it took a bit of reverse engineering and a lot of research to accomplish what I wanted successfully.

That is to have a NextCloud instance running in Docker, serving my friends and family! Sneak peak, I’ve been running this since July of 18 without any issues!

If you aren’t familiar with NextCloud or Docker, I suggest researching both technologies, before reading any further.  Especially, Docker since this article is going to assume a basic understanding of docker and docker-compose.  But since you are here I am betting that somewhat what familiar.

The only prerequisite is for you to have Docker installed. I’m not going to into details on how to this since you can run docker on OSX, Unix Systems, and Windows. There are plenty of tutorials out there for all those different platforms. For reference, I will be running Ubuntu Server on an old mac mini 🙂

There will be a series of posts that describe how to get to a production-ready instance of NextCloud and have it be secure, maintain backups, and how to approach upgrades.  Below is the first post aimed at getting you up and running for experimenting.

Apache vs FPM?!

If you’ve stumbled upon NextCloud’s GitHub repository for their Docker information, I’m sure you’re wondering whether or not you should run “apache” or “fpm”.  The “apache” image will allow you to get up and running more quickly since it has a web server baked into the image; however, I have found that the extra effort (which we’ll cover) to set up a reverse proxy has been phenomenal for maintenance and other things.

If you are planning on running this setup long term, I would suggest FPM as you’ll have more control over everything.


Let’s get into the details of how to set this up.  Below is the basic folder structure of what I have currently running.

  • nextcloud
    • app
      • Dockerfile
      • redis.config.php
      • supervisord.conf
    • db.env
    • docker-compose.yml
    • .env
    • web
      • Dockerfile
      • ngix.conf


If you’re anything like me you’re probably looking for someone to share their docker-compose file so that you can get up and running without needing to think about anything.  After all, you just want to try out NextCloud then worry about the formalities if you like it.

Well, I’m here to tell you there is a bit more to it than just copying my docker-compose.yml and getting up and running.  But trust me it is not much more. 🙂

** Note: below will not include the setup for SSL and should not be used as an external source.  We’ll go into depth on this in another post. **


version: '3'

    image: postgres:10.5
    restart: always
      - ${DATA_DIR}/db:/var/lib/postgresql/data
      - db.env
      - nextcloud

    build: ./app
    restart: always
      - ${DATA_DIR}/nextcloud:/var/www/html
      - POSTGRES_HOST=db
      - db.env
      - db
      - nextcloud
    build: ./web
    restart: always
      - 9090:80
      - ${DATA_DIR}/nextcloud:/var/www/html:ro
      - app
      - nextcloud
      - default
    image: library/redis:latest
    restart: always
      - app
      - nextcloud

Now you may notice a couple of odd things out of the gate.  Let’s take it from the top:
First, we create four services that will be used in conjunction: dbapp, web, and redis service.


For my set up I have chosen to use postgres:10.5 strictly because I have a preference for Postgres over MariaDB.

Note that NextCloud recommends using MariaDB, so choose with certainty as you will be the one maintaining.


Note that I specified 10.5 as the docker image tag.  This is because when I initially created my environment this was the latest version that NextCloud supported and when I was testing upgrades I wanted to make sure the database version was the same.

The volume declaration is pretty self-explanatory; however, I’ve chosen to supply a variable to my docker-compose via a .env file (which we’ll get into at the end).
Next, we specify the database environment file which will contain our database password and credentials (db.env).


We’ve declared a network interface named nextcloud so that we can have a separate network layer to communicate across all the services.


Now the app service is declared a bit different because it uses a custom Dockerfile to achieve its image.  This means that we will need to build the image anytime we make a change.  Here’s my current Dockerfile that lives under a folder named: app.

FROM nextcloud:14.0.0-fpm
COPY redis.config.php /usr/src/nextcloud/config/redis.config.php
COPY supervisord.conf /etc/supervisor/supervisord.conf

RUN apt-get update; \
apt-get install -y --no-install-recommends \
sudo \
supervisor \
curl \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir /var/log/supervisord /var/run/supervisord

RUN apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false;

CMD ["/usr/bin/supervisord"]

You’ll notice that there is a bit more going on here than NextCloud’s example here.  I know that the Docker purist would blast me for having this container do more than just host the NextCloud instance; however, I didn’t like that the others were using cron jobs outside the container so I decided to use supervisor to achieve my end goal.  One of which is a single container that has what it needs to stand on its own. I also installed curl to make my life easier debugging endpoints.

Now your probably tired of reading and just want to know what the two config files contain.  So here ya go:

<php $CONFIG = array ('memcache.locking' => '\\OC\\Memcache\\Redis',
 'redis' => array(
         'host' => 'redis',
         'port' => 6379,

This file is simply a configuration file for Redis and is the exact same as in NextCloud’s GitHub.

logfile_maxbytes=50MB                           ; maximum size of logfile before rotation
logfile_backups=10                              ; number of backed up logfiles



This is where the magic happens, sort of speak.  Here we define the two apps that we want running: php-fpm and cron.  The “magical” note here is that the NextCloud:fpm image ships with  This particular script uses busybox (an all-in-one utility) to invoke cron as most of us know it.  Busybox includes a plethora of other apps that serve different purposes.


Now the web image is much simpler than the app image.  This is because it is a nginx reverse proxy.  It does have its own folder named web and contains two files: Dockerfile and nginx.conf.

FROM nginx

COPY nginx.conf /etc/nginx/nginx.conf
user www-data;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/;

events {
    worker_connections  1024;

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    real_ip_header    X-Real-IP;

    #gzip  on;

    upstream php-handler {
        server app:9000;

    server {
        listen 80;

        # Add headers to serve security related headers
        # Before enabling Strict-Transport-Security headers please read into this
        # topic first.
        # add_header Strict-Transport-Security "max-age=15768000;
        # includeSubDomains; preload;";
        # WARNING: Only add the preload option once you read about
        # the consequences in This option
        # will add the domain to a hardcoded list that is shipped
        # in all major browsers and getting removed from this list
        # could take several months.
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Robots-Tag none;
        add_header X-Download-Options noopen;
        add_header X-Permitted-Cross-Domain-Policies none;
        add_header Referrer-Policy no-referrer;

        # Remove X-Powered-By, which is an information leak
        fastcgi_hide_header X-Powered-By;

        root /var/www/html;

        location = /robots.txt {
            allow all;
            log_not_found off;
            access_log off;

        # The following 2 rules are only needed for the user_webfinger app.
        # Uncomment it if you're planning to use this app.
        #rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
        #rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json
        # last;

        location = /.well-known/carddav {
            return 301 $scheme://$host/remote.php/dav;
        location = /.well-known/caldav {
            return 301 $scheme://$host/remote.php/dav;

        # set max upload size
        client_max_body_size 10G;
        fastcgi_buffers 64 4K;

        # Enable gzip but do not remove ETag headers
        gzip on;
        gzip_vary on;
        gzip_comp_level 4;
        gzip_min_length 256;
        gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
        gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/ application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

        # Uncomment if your server is build with the ngx_pagespeed module
        # This module is currently not supported.
        #pagespeed off;

        location / {
            rewrite ^ /index.php$uri;

        location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {
            deny all;
        location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) {
            deny all;

        location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {
            fastcgi_split_path_info ^(.+\.php)(/.*)$;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $fastcgi_path_info;
            # fastcgi_param HTTPS on;
            #Avoid sending the security headers twice
            fastcgi_param modHeadersAvailable true;
            fastcgi_param front_controller_active true;
            fastcgi_pass php-handler;
			fastcgi_request_buffering off;

        location ~ ^/(?:updater|ocs-provider)(?:$|/) {
            try_files $uri/ =404;
            index index.php;

        # Adding the cache control header for js and css files
        # Make sure it is BELOW the PHP block
        location ~ \.(?:css|js|woff|svg|gif)$ {
            try_files $uri /index.php$uri$is_args$args;
            add_header Cache-Control "public, max-age=15778463";
            # Add headers to serve security related headers (It is intended to
            # have those duplicated to the ones above)
            # Before enabling Strict-Transport-Security headers please read into
            # this topic first.
            # add_header Strict-Transport-Security "max-age=15768000;
            #  includeSubDomains; preload;";
            # WARNING: Only add the preload option once you read about
            # the consequences in This option
            # will add the domain to a hardcoded list that is shipped
            # in all major browsers and getting removed from this list
            # could take several months.
            add_header X-Content-Type-Options nosniff;
            add_header X-XSS-Protection "1; mode=block";
            add_header X-Robots-Tag none;
            add_header X-Download-Options noopen;
            add_header X-Permitted-Cross-Domain-Policies none;
            # Optional: Don't log access to assets
            access_log off;

        location ~ \.(?:png|html|ttf|ico|jpg|jpeg)$ {
            try_files $uri /index.php$uri$is_args$args;
            # Optional: Don't log access to other assets
            access_log off;


For the latest version checkout NextCloud’s documentation on nginx configuration.  This file is pretty boilerplate and should be updated when you upgrade to newer versions of NextCloud.


You’ll notice that we’re mapping port 9090 to port 80.  Since we’re not using a reverse proxy with SSL (yet), we’re choosing to run on port 9090 for local testing.


We specify two network interfaces here because we want this service to communicate with the outside world and the two additional containers.  The default interface gets us the communication to the “outside world” and the nextcloud interface gets us communication across the different containers.


This container is pretty simple and straightforward. Nothing custom or unique, just using the latest version of the Redis image.  We have declared the nextcloud network interface so that it can communicate with the web image too.

Environmental File (.env)

Now you’ve probably noticed the usage of “${DATA_DIR}” and wondering why I’m using a volume mount, instead of docker volume.  The reason behind this is that I wanted to have the data housed on a particular partition on my host that Docker was not installed on.

Nevertheless, we need to create an environment file which is called “.env” and looks like:

# General settings #
# Directory on the host where all the data will be stored. It will be automatically created if it doesn't exist.
DATA_DIR=<the path you want to store the files>

Database Environment File (db.env)

Lastly, we need to create our db.env which houses the credentials for the database.  Regardless of which database engine you decide to use, you will need this file, but the contents will vary by the engine. Below are the variables for Postgres:



Bringing It Home

Now that you have all the files necessary in place.  You should be ready to finally take it for a spin. You can do so by executing from within the nextcloud folder:

docker-compose up

This will start all the services and you can Cntrl-C when you’re ready to tear it down.   Don’t worry about losing any files you’ve uploaded for a test as they should remain on disk.  If you want to run and detach, use the “-d” parameter when executing the command above.


Stay tuned for the next post in the upcoming days where we explore using Let’s Encrypt and a reverse proxy to securely host our NextCloud instance!!

4 thoughts on “NextCloud Docker Tutorial – Part I”

    1. Hi Jan,

      It has been a while since I wrote the original tutorial. I do plan on following up with a Part II and a review of running NextCloud for the past year. Stay tuned!

  1. Thank you for this tutorial! Just wat I was looking for. Just a few minor details:

    In app/Dockerfile you copy the file redis.config.php while you name the mentioned file redis.conf.php (mind the missing ‘ig’)

    I kept on getting apt/dpkg-config errors when installing supervisor:

    Configuration file ‘/etc/supervisor/supervisord.conf’
    ==> File on system created by you or by a script.
    ==> File also in package provided by package maintainer.
    What would you like to do about it ? Your options are:
    Y or I : install the package maintainer’s version
    N or O : keep your currently-installed version
    D : show the differences between the versions
    Z : start a shell to examine the situation
    The default action is to keep your current version.
    *** supervisord.conf (Y/I/N/O/D/Z) [default=N] ? dpkg: error processing package supervisor (–configure):
    end of file on stdin at conffile prompt
    Processing triggers for libc-bin (2.28-10) …
    Errors were encountered while processing:

    Ifixed it by creating an extra file ‘app/dpkg_local.conf’ with the following contents:

    Dpkg::Options {

    And added a COPY command tot the ‘add/Dockerfile’:
    -> COPY dpkg_local.conf /etc/apt/apt.conf.d/local

    Now the script runs without problem. Now just need to wait for part 2, or figure out how to get NextCloud in there 😉

    1. Hi Wouter,

      It sounds like you may be basing the app/Dockerfile off a different base? Is is possible that you copied the ‘supervisord.conf’ before running the apt install? Normally that prompt occurs during an upgrade because a file already exists and the maintainer wasn’t expecting it to already be there.

      Nevertheless, once you have that image built and running you should have a running version of NextCloud that is accessible and once you open the web page to the site, it should walk you through the rest of the configuration.

      Best of luck!

Leave a Reply

Your email address will not be published. Required fields are marked *