Do not speak Portuguese? Translate this site with Google or Bing Translator
Docker, Laravel, Websockets, NGINX, and Let's Encrypt

Posted on: September 04, 2024 05:17 PM

Posted by: Renato

Categories:

Views: 221

Deploying with Docker Compose, Laravel, Websockets, NGINX, and Let's Encrypt.

Deploying with Docker Compose, Laravel, Websockets, NGINX, and Let's Encrypt.

Heondo Kim - May 9, 2021 at 11:24 am
  1. The Primary Bits
  2. Getting an SSL cert with Let's Encrypt, Nginx, and Docker
  3. 403 Invalid Signature and Email Verification
  4. The problem with websockets? Nginx of course!
  5. Final words

So, I just got through the process of deploying a Docker application with Websockets, Nginx, Let's Encrypt.

To put it lightly, it was rough. Lots of hair-pulling days and nights of little to no progress, but a whole lot of lessons on what does not work.

After many days of going four search results deep with all purple links, my coworker joked that I should write an article on deploying with this configuration. Given the amount of work it required, I think it should be documented for others who might be running a similar configuration.

Project layout and structure

If you are here, I am assuming you have a Dockerized Laravel application that you are attempting to deploy to some server like AWS EC2, Digital Ocean, GCP, Azure, etc. But what's this, your Nginx configuration is causing issues with SSL/Websockets? I hope this can help a bit.

The primary bits

  • Nginx configuration file
  • Docker Compose file
  • Environment variables file

These are the configuration files pre-deployment, some oddities:

Mapping port 6001 to our local machine to connect to websockets, I think I need to configure the nginx proxy for that too but it's easier in development to just do localhost. It will be fixed in production, along with only exposing port 443
docker-compose.yml
version: '3.7'
services:
  myapp-app:
    build:
      args:
        user: sgadmin
        uid: 1000
      context: ./
      dockerfile: Dockerfile
    image: myapp
    container_name: myapp-app
    restart: unless-stopped
    working_dir: /var/www/
    volumes:
      - ./:/var/www
    networks:
      - myapp

  myapp-db:
    image: mariadb
    container_name: myapp-db
    depends_on:
      - myapp-redis
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USERNAME}
      SERVICE_TAGS: dev
      SERVICE_NAME: mysql
    volumes:
      - ./docker-compose/mysql:/docker-entrypoint-initdb.d
    networks:
      - myapp

  myapp-websockets:
    image: myapp
    command: ['php', 'artisan', 'websockets:serve']
    container_name: myapp-websockets
    restart: unless-stopped
    working_dir: /var/www/
    volumes:
      - ./:/var/www
    ports:
      - ${LARAVEL_WEBSOCKETS_PORT}:6001
    networks:
      - myapp

  myapp-nginx:
    image: nginx:alpine
    container_name: myapp-nginx
    restart: unless-stopped
    ports:
      - ${APP_PORT}:80
    volumes:
      - ./:/var/www
      - ./docker-compose/nginx/local:/etc/nginx/conf.d/
    networks:
      - myapp

networks:
  myapp:
    driver: bridge
docker-compose/nginx/local/nginx.conf
server {
    listen 80;
    index index.php index.html;
    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    root /var/www/public;
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass myapp-app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
    location / {
        try_files $uri $uri/ /index.php?$query_string;
        gzip_static on;
    }
}

.env
APP_NAME=MyApp
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_PORT=8000 other if you have conflict

NODE_ENV=development

SANCTUM_STATEFUL_DOMAINS=localhost,127.0.0.1,localhost:8000,127.0.0.1:8000 # update if port changes

LARAVEL_WEBSOCKETS_HOST=localhost
LARAVEL_WEBSOCKETS_PORT=6001
LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT=
LARAVEL_WEBSOCKETS_SSL_LOCAL_PK=
LARAVEL_WEBSOCKETS_SSL_PASSPHRASE=

BROADCAST_DRIVER=pusher

PUSHER_APP_ID=myapp-websockets-channel
PUSHER_APP_KEY=myapp-wbsocks
PUSHER_APP_SECRET=myapp@@
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MIX_PUSHER_APP_TLS="false"

# All other env variables...

With this configuration, locally, websockets works fine. My client app can listen in and react to real time events. However, once we attempt to deploy we run into a whole bunch of issues.

Getting an SSL cert with Let's Encrypt, Nginx, and Docker

It was simple at first, buy a server, clone the repo, and spin up the containers like I do locally. Yay! Well, http works which is unacceptable in any modern website.

So I did what any developer does, Google how to do X with Y. After failing, I looked up how to do X with Y and Z, and X with ❅. More failing, until I stumbled upon a wonderful article with many claps and a Github Repo for the script file with many stars to go with it.

I followed them to a T and what do you know, I was able to visit my HTTPS website, with a wonderful lock. Fiddle with your .env files and Laravel configuration to match the new production/HTTPS environment. Now, here is where the 403 email invalid signature appeared and caused my client to throw various errors when attempting to connect to the websockets channel.

403 Invalid Signature and Email Verification

When testing the email verification feature, I ran into the same issue as here. I couldn't figure it out until I realized I did not modify the Nginx configuration file to fully work through HTTPS.

So I created a new NGINX configuration file ./docker-compose/nginx/prod/nginx.conf with the contents

After following the tutorial, our files should look something like this, with a few differences
./docker-compose/nginx/prod/nginx.conf
server {
    listen 80;
+   listen [::]:80;

+   location /.well-known/acme-challenge/ {
+       root /var/www/certbot;
+   }

+   return 301 https://$host$request_uri;

-   index index.php index.html;
-     error_log  /var/log/nginx/error.log;
-     access_log /var/log/nginx/access.log;
-     root /var/www/public;
-     location ~ \.php$ {
-         try_files $uri =404;
-         fastcgi_split_path_info ^(.+\.php)(/.+)$;
-         fastcgi_pass myapp-app:9000;
-         fastcgi_index index.php;
-         include fastcgi_params;
-         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
-         fastcgi_param PATH_INFO $fastcgi_path_info;
-     }
-     location / {
-         try_files $uri $uri/ /index.php?$query_string;
-         gzip_static on;
-     }

}

+server {
+    listen 443 ssl http2;
+    listen [::]:443 ssl http2;
+
+    root /var/www/public;
+    server_tokens off;
+
+    access_log /var/log/nginx/access.log;
+
+    add_header X-Frame-Options "SAMEORIGIN";
+    add_header X-XSS-Protection "1; mode=block";
+    add_header X-Content-Type-Options "nosniff";
+
+    index index.html index.php;
+
+
+    location / {
+        try_files $uri $uri/ /index.php?$query_string;
+        gzip_static on;
+    }
+
+    location ~ \.php$ {
+        fastcgi_split_path_info ^(.+\.php)(/.+)$;
+        fastcgi_pass myapp-app:9000;
+        fastcgi_index index.php;
+        include fastcgi_params;
+        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+        fastcgi_param PATH_INFO $fastcgi_path_info;
+    }
+
+    client_max_body_size 20M;
+
+    gzip on;
+    gzip_disable "msie6";
+
+    tcp_nopush on;
+    tcp_nodelay on;
+
+    charset utf-8;
+
+    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
+    include /etc/letsencrypt/options-ssl-nginx.conf;
+    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
+}
docker-compose.yml
version: '3.7'
services:
  # remaining services above

  myapp-nginx:
    image: nginx:alpine
    container_name: myapp-nginx
    restart: unless-stopped
    ports:
      - ${APP_PORT}:80
+     - ${APP_PORT}:443
    volumes:
      - ./:/var/www
-     - ./docker-compose/nginx/local:/etc/nginx/conf.d/
+     - ./docker-compose/nginx/prod:/etc/nginx/conf.d/
+     - ./docker-compose/certbot/conf:/etc/letsencrypt
+     - ./docker-compose/certbot/www:/var/www/certbot

    networks:
      - myapp

+  myapp-certbot:
+    image: certbot/certbot
+    container_name: myapp-certbot
+    volumes:
+      - ./docker-compose/certbot/conf:/etc/letsencrypt
+      - ./docker-compose/certbot/www:/var/www/certbot
+    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
+    networks:
+      - myapp

# Volumes and networks unchanged
# add certbot service and volume mappings to compose
.env
- APP_PORT=80
+ APP_PORT=443
- APP_URL=http://localhost
+ APP_URL=https://myapp.com
Once all traffic was forwarded directly to 443, instead of processing a request through HTTP first and then forwarding it to HTTPS after, the invalid signature went away. This was because passing through two proxies cause the signature to change and thus we received an invalid signature error.
I praise Travis Ryan for helping me figure this out. Before him my life was this Google page
purple-google-results

So that was solved, but I realized my websockets connection kept failing now that our SSL configuration was complete. It took weeks to diagnose and resolve this issue, but it was also done.

The problem with websockets? Nginx of course!

Once we moved from local to production and our app was being served with SSL, the application could no longer communicate to our websockets service. It took weeks of diagnosing but it was unfortunately quite simple...

The only exposed port in our application is 443, all traffic must be reverse proxied. Begin by removing port mapping for our websockets container and modify our Nginx configuration to reverse proxy the websockets traffic as well.
docker-compose.yml
version: '3.7'
services:
  ...
  myapp-websockets:
    image: myapp
    command: ['php', 'artisan', 'websockets:serve']
    container_name: myapp-websockets
    restart: unless-stopped
    working_dir: /var/www/
    volumes:
      - ./:/var/www
-   ports:
-     - ${LARAVEL_WEBSOCKETS_PORT}:6001
    networks:
      - myapp
docker-compose/nginx/myapp.conf
...
server {
    ...
    # in the 443 block!
    ...

+    location /app/ {
+        proxy_pass             http://myapp-websockets:6001;
+        # THIS IS IMPORTANT, host is container name
+        proxy_read_timeout     60;
+        proxy_connect_timeout  60;
+        proxy_redirect         off;
+
+    #     Allow the use of websockets
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection 'upgrade';
+        proxy_set_header Host $host;
+        proxy_cache_bypass $http_upgrade;
+     }

    # The below may not be necessary

+    location = /favicon.ico { access_log off; log_not_found off; }
+    location = /robots.txt  { access_log off; log_not_found off; }
+
+    location ~ \/.ht {
+        deny all;
+    }
}
.env
- LARAVEL_WEBSOCKETS_HOST=localhost
+ LARAVEL_WEBSOCKETS_HOST=myapp-websockets # from localhost to docker container name
LARAVEL_WEBSOCKETS_PORT=6001

Those were the primary changes to take our application from development to production using Laravel, Docker Compose, Nginx, and Websockets.

I may have missed some external deployment steps regarding Laravel steps but you have to follow the documentation for that. The infrastructure needs to be improved, we need to move our database from a Docker container into the cloud, same with Redis and even Websockets as we scale, really any X-as-a-service. However, those are typically a matter of changing environment variables and configuration files again, but just getting the application deployed via HTTPS was a big step. I saw many people running similar issues like myself, so maybe I will pop up in a search.

- https://github.com/heondo/portfolio/blob/main/content/articles/laravel-websockets-nginx-docker.md

- https://www.heondokim.com/articles/laravel-websockets-nginx-docker


11

Share

Donate to Site


About Author

Renato

Developer

Add a Comment
Comments 0 Comments

No comments yet! Be the first to comment

Blog Search


Categories

OUTROS (16) Variados (109) PHP (133) Laravel (171) Black Hat (3) front-end (29) linux (114) postgresql (39) Docker (28) rest (5) soap (1) webservice (6) October (1) CMS (2) node (7) backend (13) ubuntu (56) devops (25) nodejs (5) npm (3) nvm (1) git (8) firefox (1) react (7) reactnative (5) collections (1) javascript (7) reactjs (8) yarn (0) adb (1) Solid (2) blade (3) models (1) controllers (0) log (1) html (2) hardware (3) aws (14) Transcribe (2) transcription (1) google (4) ibm (1) nuance (1) PHP Swoole (5) mysql (31) macox (4) flutter (1) symfony (1) cor (1) colors (2) homeOffice (2) jobs (3) imagick (2) ec2 (1) sw (1) websocket (2) markdown (1) ckeditor (1) tecnologia (14) faceapp (1) eloquent (14) query (4) sql (40) ddd (3) nginx (9) apache (4) certbot (1) lets-encrypt (3) debian (12) liquid (1) magento (2) ruby (1) LETSENCRYPT (1) Fibonacci (1) wine (1) transaction (1) pendrive (1) boot (1) usb (1) prf (1) policia (2) federal (1) lucena (1) mongodb (4) paypal (1) payment (1) zend (1) vim (4) ciencia (6) js (1) nosql (1) java (1) JasperReports (1) phpjasper (1) covid19 (1) saude (1) athena (1) cinnamon (1) phpunit (2) binaural (1) mysqli (3) database (42) windows (6) vala (1) json (2) oracle (1) mariadb (4) dev (12) webdev (24) s3 (4) storage (1) kitematic (1) gnome (2) web (2) intel (3) piada (1) cron (2) dba (18) lumen (1) ffmpeg (2) android (2) aplicativo (1) fedora (2) shell (4) bash (3) script (3) lider (1) htm (1) csv (1) dropbox (1) db (3) combustivel (2) haru (1) presenter (1) gasolina (1) MeioAmbiente (1) Grunt (1) biologia (1) programming (22) performance (3) brain (1) smartphones (1) telefonia (1) privacidade (1) opensource (3) microg (1) iode (1) ssh (3) zsh (2) terminal (3) dracula (1) spaceship (1) mac (2) idiomas (1) laptop (2) developer (37) api (5) data (1) matematica (1) seguranca (2) 100DaysOfCode (9) hotfix (1) documentation (1) laravelphp (10) RabbitMQ (3) Elasticsearch (1) redis (2) Raspberry (4) Padrao de design (4) JQuery (1) angularjs (4) Dicas (43) Kubernetes (3) vscode (2) backup (1) angular (3) servers (2) pipelines (1) AppSec (1) DevSecOps (4) rust (1) RustLang (1) Mozilla (1) algoritimo (1) sqlite (1) Passport (2) jwt (5) security (2) translate (1) kube (2) iot (1) politica (2) bolsonaro (1) flow (1) podcast (1) Brasil (1) containers (3) traefik (1) networking (1) host (1) POO (2) microservices (2) bug (1) cqrs (1) arquitetura (3) Architecture (4) sail (3) militar (1) artigo (1) economia (1) forcas armadas (1) ffaa (1) autenticacao (2) autorizacao (2) authentication (4) authorization (3) NoCookies (1) wsl (4) memcached (1) macos (2) unix (2) kali-linux (1) linux-tools (5) apple (1) noticias (2) composer (1) rancher (1) k8s (1) escopos (1) orm (1) jenkins (4) github (5) gitlab (3) queue (1) Passwordless (1) sonarqube (1) phpswoole (1) laraveloctane (1) Swoole (1) Swoole (1) octane (1) Structurizr (1) Diagramas (1) c4 (1) c4-models (1) compactar (1) compression (1) messaging (1) restfull (1) eventdrive (1) services (1) http (1) Monolith (1) microservice (1) historia (1) educacao (1) cavalotroia (1) OOD (0) odd (1) chatgpt (1) openai (3) vicuna (1) llama (1) gpt (1) transformers (1) pytorch (1) tensorflow (1) akitando (1) ia (1) nvidia (1) agi (1) guard (1) multiple_authen (2) rpi (1) auth (1) auth (1) livros (2) ElonMusk (2) Oh My Zsh (1) Manjaro (1) BigLinux (2) ArchLinux (1) Migration (1) Error (1) Monitor (1) Filament (1) LaravelFilament (1) replication (1) phpfpm (1) cache (1) vpn (1) l2tp (1) zorin-os (1) optimization (1) scheduling (1) monitoring (2) linkedin (1) community (1) inteligencia-artificial (2) wsl2 (1) maps (1) API_KEY_GOOGLE_MAPS (1) repmgr (1) altadisponibilidade (1) banco (1) modelagemdedados (1) inteligenciadedados (4) governancadedados (1) bancodedados (2) Observability (1) picpay (1) ecommerce (1) Curisidades (1) Samurai (1) KubeCon (1) GitOps (1) Axios (1) Fetch (1) Deepin (1) vue (4) nuxt (1) PKCE (1) Oauth2 (2) webhook (1) TypeScript (1) tailwind (1) gource (2)

New Articles



Get Latest Updates by Email